useState with TypeScript cover
BCiriak Avatar
by BCiriakMAR 16, 2023 | 11 min read

UseState with TypeScript

UseState is one of the simple but important React hooks. We handle state in functional components with this hook. But what is *state* in React components? Let us have a look.


What is the state in React.js Component?

We can think of a state as a small piece of memory that holds our component's data. It can be a variable called showModal. This variable holds data (boolean) that tell our component if it should show a modal.

This value usually changes on user input. The user clicks on the button and the modal will be shown. Let's look at a basic state example:

App.tsx
import { useState } from 'react'

function App() {
  const [showModal, setShowModal] = useState(false)

  const toggleModal = () =>  {
    setShowModal(!showModal)
  }

  return (
    <div>
      <button onClick={toggleModal}>Toggle Modal</button>
      {showModal && <p>Modal</p>}
    </div>
  )
}

export default App

First, we import useState hook from React and initialise it at the beginning of our component. As you can see, we use array destructuring syntax to create the state.

App.tsx
const [showModal, setShowModal] = useState(false)

The first element is the piece of state in our case boolean called showModal and the second element is the so-called dispatch function that updates this piece of state. Names of these elements should follow this naming convention: name and setName.

Note that we gave the useState hook one parameter, and this is called the initial state. With a simple state like this (boolean), TypeScript will infer the type of this initial state, but we can type our useState if we wanted to. This would be handy with more complex data, which we will get to a bit later.

Just an aside, we can handle more complex data with another React hook, useReducer. Learn more about useReducer with TypeScript.

Next, we have a simple function toggleModal, which calls setShowModal function with the opposite boolean value (so that we toggle the boolean).

App.tsx
const toggleModal = () =>  {
  setShowModal(!showModal)
}

And at last, we have our JSX with a simple button and conditionally rendered paragraph.

App.tsx
return (
  <div>
    <button onClick={toggleModal}>Toggle Modal</button>
    {showModal && <p>Modal</p>}
  </div>
)

Aside: useState vs variable

Some newcomers to the world of JavaScript and React might not realize the simple but very important truth about React. I know I was a bit confused when first started.

Why can't we use a regular JavaScript variable and do something like this?

App.tsx
function App() {
  let showModal = false

  const toggleModal = () =>  {
    showModal = !showModal
  }

  return (
    <div>
      <button onClick={toggleModal}>Toggle Modal</button>
      {showModal && <p>Modal</p>}
    </div>
  )
}

export default App

Well, simply because it will not work. React will not re-render the component.

If we were to write this piece of code in vanilla JavaScript/TypeScript, it could look something like this:

app.ts
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
  <div id="component">
    <button id="toggler">Toggle Modal</button>
  </div>
`

let component = document.querySelector<HTMLButtonElement>('#component')!
let button = document.querySelector<HTMLButtonElement>('#toggler')!
let modal = document.createElement('p')
modal.innerHTML = "Modal"
let showModal = false

const toggleModal = () => {
  showModal = !showModal

  if (showModal) {
    component.appendChild(modal)
  } else {
    component.removeChild(modal)
  }
}

button.addEventListener('click', () => toggleModal())

Which is definitely worse looking than our React component.

So it is important to realize, React does a ton of work for us behind the scenes. One of the big ones is, watching for changes and re-rendering UI. Aside end!

When does React re-renders component?

If you want great in-depth look at how and when React re-renders, have a look at React re-renders guide.

There are 4 reasons why React will re-render a component:

  • state changes
  • parent re-renders
  • context changes
  • hook changes

and useState hook is one of the states changes that React watches.

useState with TypeScript

Let's have a look at 2 more use cases with a bit more complex data. One common use case is fetching data and displaying it in a component.

Fetch data and useState

First, we define data coming from API. I am using a dummy JSON endpoint that returns objects looking like this: { id: string, username: string}. This is exactly how our User type is gonna look like.

App.tsx
type User = {
  id: string
  username: string
}

Now we can type our useState hook with this custom type and initialize it to an empty array.

App.tsx
const [users, setUsers] = useState<User[]>([])

// to specify what types will our array hold, we do it by writing the type and []
// array of strings => string[]
// array of Users => User[]

This piece of state is gonna hold our fetched API data. We fetch the data when the component loads, which means we want to call the API in another React hook useEffect.

App.tsx
useEffect(() => {
  const fetchUsers = async () => {
    const data = await fetch('https://my-json-server.typicode.com/jstopics/placeholders/users')
    setUsers(await data.json())
  }

  fetchUsers()
}, [])

By providing an empty array as a second argument in useEffect, we are telling it to run only once.

And the last thing is to conditionally show our data if the User array is not empty. Our IDE will nicely suggest properties of the user thanks to TypeScript.

And the whole component:

App.tsx
import { useState, useEffect } from 'react'

type User = {
  id: string
  username: string
}

function App() {
  const [users, setUsers] = useState<User[]>([])

  useEffect(() => {
    const fetchUsers = async () => {
      const data = await fetch('https://my-json-server.typicode.com/jstopics/placeholders/users')
      setUsers(await data.json())
    }

    fetchUsers()
  }, [])
  
  return (
    <div>
      {users.length > 0 && <ul>
        {users.map(user => <li key={user.id}>{user.username}</li>)}
      </ul>}
    </div>
  )
}

export default App

Form data with useState and TypeScript

Another common use case is handling form data with useState. Simple forms can be easily handled by useState hook.

Again we start with defining the type of data we want to handle with our form. Let's say it's going to be a login form:

Form.tsx
type FormData = {
  username: string
  password: string
}

And now we want to define some initial state of the form. This will also help us to reset the form after we send the data.

Form.tsx
const initialState: FormData = {
  username: '',
  password: ''
}

These are just an empty string, as we would expect to see in the login form 😑

We can now initialise our state:

Form.tsx
const [formData, setFormData] = useState<FormData>(initialState)

Be careful with the FormData type, IDE can pull in the original FormData type. Also, note the initialState parameter. And the JSX:

Form.tsx
return (
  <form>
    <div>
      <label htmlFor="username">Username</label>
      <input 
        type="text" 
        name="username"
        value={formData.username} 
        onChange={handleChange} 
      />
    </div>
    <div>
      <label htmlFor="password">Password</label>
      <input 
        type="password" 
        name="password"
        value={formData.password} 
        onChange={handleChange} 
      />
    </div>
    <button 
      type="button" 
      onClick={() => {console.log(formData)}}
    >
      Log In
    </button>
  </form>
)

We just wire our input elements with our state. The last piece left to do is to implement the onChange event. We can create one function, that will look at the input name attribute and update the piece of state with that key.

Form.tsx
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  e.preventDefault()
  const value = e.target.value
  const prop = e.target.name

  setFormData({...state, [prop]: value})
}

The hardest part here is probably figuring out the type of the event if you are just starting with TypeScript and React. The thing is, there is not a piece of good advice on how to find the correct type, just search the internet. Don't worry about that, with some practice it will come easy.

Then we need to preventDefault because the default behavior of the form is to reload the whole page after it is submitted. Next, we assign our variables and update the state with little object destructuring trick {...state, [prop]: value}.

And the whole component:

Form.tsx
import { useState } from 'react'

type FormData = {
  username: string
  password: string
}

const initialState: FormData = {
  username: '',
  password: ''
}

function Form() {
  const [formData, setFormData] = useState<FormData>(initialState)

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    e.preventDefault()
    const value = e.target.value
    const prop = e.target.name

    setFormData({...state, [prop]: value})
  }

  return (
    <form>
      <div>
        <label htmlFor="username">Username</label>
        <input 
          type="text" 
          name="username"
          value={formData.username} 
          onChange={handleChange} 
        />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input 
          type="password" 
          name="password"
          value={formData.password} 
          onChange={handleChange} 
        />
      </div>
      <button 
        type="button" 
        onClick={() => {console.log(formData)}}
      >
        Log In
      </button>
    </form>
  )
}

export default Form

I hope you enjoyed this article, if you did leave a reaction below, I would much appreciate it 🙃

Keep learning and see you in the next one!

Connect with me on social media ✨

Join my newsletter, to receive JavaScript, TypeScript, React.js and more news, tips and other goodies right into your mail box 📥. You can unsubscribe at any time.

©2024 JSTopics. All rights reserved.