useState with TypeScript cover
BCiriak Avatar
by BCiriakMAR 16, 2023 | 12 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.tsxtypescript
1import { useState } from 'react'
2
3function App() {
4  const [showModal, setShowModal] = useState(false)
5
6  const toggleModal = () =>  {
7    setShowModal(!showModal)
8  }
9
10  return (
11    <div>
12      <button onClick={toggleModal}>Toggle Modal</button>
13      {showModal && <p>Modal</p>}
14    </div>
15  )
16}
17
18export default App

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

App.tsxtypescript
4const [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.tsxtypescript
6const toggleModal = () =>  {
7  setShowModal(!showModal)
8}

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

App.tsxtypescript
10return (
11  <div>
12    <button onClick={toggleModal}>Toggle Modal</button>
13    {showModal && <p>Modal</p>}
14  </div>
15)

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.tsxtypescript
1function App() {
2  let showModal = false
3
4  const toggleModal = () =>  {
5    showModal = !showModal
6  }
7
8  return (
9    <div>
10      <button onClick={toggleModal}>Toggle Modal</button>
11      {showModal && <p>Modal</p>}
12    </div>
13  )
14}
15
16export 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.tstypescript
1document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
2  <div id="component">
3    <button id="toggler">Toggle Modal</button>
4  </div>
5`
6
7let component = document.querySelector<HTMLButtonElement>('#component')!
8let button = document.querySelector<HTMLButtonElement>('#toggler')!
9let modal = document.createElement('p')
10modal.innerHTML = "Modal"
11let showModal = false
12
13const toggleModal = () => {
14  showModal = !showModal
15
16  if (showModal) {
17    component.appendChild(modal)
18  } else {
19    component.removeChild(modal)
20  }
21}
22
23button.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.tsxtypescript
3type User = {
4  id: string
5  username: string
6}

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

App.tsxtypescript
9const [users, setUsers] = useState<User[]>([])
10
11// to specify what types will our array hold, we do it by writing the type and []
12// array of strings => string[]
13// 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.tsxtypescript
11useEffect(() => {
12  const fetchUsers = async () => {
13    const data = await fetch('https://my-json-server.typicode.com/jstopics/placeholders/users')
14    setUsers(await data.json())
15  }
16
17  fetchUsers()
18}, [])

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.tsxtypescript
1import { useState, useEffect } from 'react'
2
3type User = {
4  id: string
5  username: string
6}
7
8function App() {
9  const [users, setUsers] = useState<User[]>([])
10
11  useEffect(() => {
12    const fetchUsers = async () => {
13      const data = await fetch('https://my-json-server.typicode.com/jstopics/placeholders/users')
14      setUsers(await data.json())
15    }
16
17    fetchUsers()
18  }, [])
19  
20  return (
21    <div>
22      {users.length > 0 && <ul>
23        {users.map(user => <li key={user.id}>{user.username}</li>)}
24      </ul>}
25    </div>
26  )
27}
28
29export 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.tsxtypescript
3type FormData = {
4  username: string
5  password: string
6}

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.tsxtypescript
8const initialState: FormData = {
9  username: '',
10  password: ''
11}

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

We can now initialize our state:

Form.tsxtypescript
14const [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.tsxtypescript
24return (
25  <form>
26    <div>
27      <label htmlFor="username">Username</label>
28      <input 
29        type="text" 
30        name="username"
31        value={formData.username} 
32        onChange={handleChange} 
33      />
34    </div>
35    <div>
36      <label htmlFor="password">Password</label>
37      <input 
38        type="password" 
39        name="password"
40        value={formData.password} 
41        onChange={handleChange} 
42      />
43    </div>
44    <button 
45      type="button" 
46      onClick={() => {console.log(formData)}}
47    >
48      Log In
49    </button>
50  </form>
51)

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.tsxtypescript
16const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
17  e.preventDefault()
18  const value = e.target.value
19  const prop = e.target.name
20
21  setFormData({...state, [prop]: value})
22}

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.tsxtypescript
1import { useState } from 'react'
2
3type FormData = {
4  username: string
5  password: string
6}
7
8const initialState: FormData = {
9  username: '',
10  password: ''
11}
12
13function Form() {
14  const [formData, setFormData] = useState<FormData>(initialState)
15
16  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
17    e.preventDefault()
18    const value = e.target.value
19    const prop = e.target.name
20
21    setFormData({...state, [prop]: value})
22  }
23
24  return (
25    <form>
26      <div>
27        <label htmlFor="username">Username</label>
28        <input 
29          type="text" 
30          name="username"
31          value={formData.username} 
32          onChange={handleChange} 
33        />
34      </div>
35      <div>
36        <label htmlFor="password">Password</label>
37        <input 
38          type="password" 
39          name="password"
40          value={formData.password} 
41          onChange={handleChange} 
42        />
43      </div>
44      <button 
45        type="button" 
46        onClick={() => {console.log(formData)}}
47      >
48        Log In
49      </button>
50    </form>
51  )
52}
53
54export 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!

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.