Ethan Mick
Back to guide home

Managing State

In the last section, you learned how to pass information to components with props. These props are read-only. They cannot change, which is a real bummer since you want to add to-dos to your application. While props are read-only, the state is mutable.

Modern React has its own way of handling state. This will bring you through a journey of learning about hooks and component lifecycle. There are third-party libraries you can use to help manage state, but they all use the same principles you will learn here.

For now, the idea is to use the tools React provides to finally make the app mutable.

Onward!

Hooks

Hooks are new in React 16. A hook is a function that has some unique properties in the React lifecycle, depending on which hook you are using.

The hook we will start with is useState, which creates a constant value representing the current value, and a setter to change it. The syntax for it is:

import { useState } from 'react'
const [name, setName] = useState<string>('')

The TypeScript type in < > defines the type. TypeScript can infer many types so it is not strictly necessary. However, you will find it useful when handling more complicated objects and arrays. The const defines these variables as constant, you cannot change them in the execution of the function. The function useState returns an array with two variables, the current value of the state and a function that will change that state. In modern JavaScript, you should always destructure these into their parts. Name them anything you want, such as [user, setUser].

What's in a name?

The function you call from React is useState, which really could have been named anything. There is nothing unique about the name except convention. React uses the prefix use to define a function as a hook. Hooks have unique properties and by following this prefix your linter and tools can help ensure your code follows those guidelines.

But why hooks?

This might seem overly complicated. In class-based projects, you would have something more like:

class MyComponent {
public name: string = ''
}

And the DOM would use value in some way, perhaps changing it on a button click.

<button onclick={() => (component.name = 'Ethan')}>Button</button>

This isn't how React works. The markup and state live in the same function, and that function is stateless. It doesn't have a this property or a way to keep any data around after it returns. The function runs and returns the value. How do you add state to a stateless function?

You do that with hooks. To understand hooks better, you need to understand the component lifecycle.

Component Lifecycle

Modern React uses stateless components for everything. Using hooks, you can tap into the lifecycle methods you need to manage this lifecycle. You'll learn the details of these later, but it's important to understand the basics of the stateless lifecycle.

Take the following example component:

const ExampleComponent = ({ name }) => {
console.log('Rendered', new Date())
return <span>{name}</span>
}

When the application starts, all components are unmounted to start. They have never been rendered before.

The first time the component is mounted, it will be rendered for the first time. This will be the first time Rendered is executed.

Whenever the name prop changes, this component will re-render, executing Rendered again with the new time.

When you add state to a component, you add another trigger to cause the component to re-render.

const ExampleComponent = ({ label }) => {
const [name, setName] = useState('')
console.log('Rendered', new Date())
return (
<div>
<label>{label}</label>
<input value={name} onChange={(e) => setName(e.target.value)} />
</div>
)
}

Anytime the label prop changes, this code will run again. Anytime the input changes it will update the name, this code will run again.

The magic of useState('') is that the default value of '' is only respected the first time this component is mounted. If the label were to change and the code ran again, the state would not revert to the empty string. It keeps whichever value it was last set to.

Finally, when you no longer render the component then the component is unmounted.

Learning useState

const [name, setName] = useState('')

Each time the component renders, the first parameter (name, above) will hold the current value for the state. If you want to change it, you cannot simply do:

name = 'Ethan'
  1. The variables are declared as const above, and cannot be changed.
  2. Even if you changed the above to be var instead of const the changed value won't stick around. When the function renders again, the value will be the original value.

Instead of changing the name directly, the second parameter returned by useState is the setter for the value. You call this function to set the state. This will cause the component to re-render, and when it does, the state will have the new value.

This is how, in an entirely stateless function, you can track state.

Other Hooks

There are other hooks that you will learn about in future chapters. These hooks tie into the lifecycle of components. You can also build custom hooks that use hooks, allowing for complex behavior to be modeled simply.

State in the to-do app

Anything that changes is state. In your to-do app, there will be two things that change.

  1. The user will input a string that will be for a new to-do. The user typing the strings is tracked as state.
  2. The to-dos themselves. The user can add and complete (remove) to-dos.

You'll start with the first and work toward the second.

User Input

The user needs to enter a new to-do in a text field. A text field can be controlled or uncontrolled in React. Uncontrolled is any user input that is not wired up to React. This is the default, but it makes it very hard to get the value from the input.

To make that easier, React lets you control the input.

To do that with a text field, you will pull the state out of the input and store it in an external place.

// Uncontrolled, no way to know what the user typed
<input type="text" />
// Controlled, pull the state out.
const [text, setText] = useState('')
// In your JSX
<input type="text" value={text} onChange={ (e) => setText(e.target.value) } />

This puts the value of the input in the state of text. Once you've done that, you can easily access the value and store it.

To manage this state, it's a good idea to toss it into a new component.

const AddTodo = () => {
const [text, setText] = useState('')
return (
<div>
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<button>Add</button>
</div>
)
}

This component set's up the input state and tracks it when the user types in the text box. Then, it has a button that when clicked should trigger the to-do to be added to the list.

You can use this component in your to-do list component:

const TodoList = ({ todos }: TodoListProps) => (
<div>
<AddTodo />
<ul>
{todos.map(({ text }, index) => (
<TodoListItem key={index} text={text} />
))}
</ul>
</div>
)

This will show the input and button at the top of the list. You can also put it at the bottom if you prefer.

It's time to add state to the To-do list as well. Up until this point, you've been working on a hard-coded set of to-dos. This has let you test and build your application quickly, but it's not a real to-do app until you can add your own.

The state you need to add is the array of Todos. Instead of passing those in as immutable props, the list will manage their to-dos. This makes sense from a product perspective because a user may have many different to-do lists.

So instead of passing in the to-dos, you add state for them:

const [todos, setTodos] = useState<Todo[]>([])

Since the array is empty to start (feel free to add defaults if you want), TypeScript doesn't know what the objects in the array will be. You solve that by adding the generic <Todo[]> code, which tells TypeScript this array will be of Todo objects.

Then you can simply use them like the prop! The final component looks like:

const TodoList = () => {
const [todos, setTodos] = useState<Todo[]>([])
return (
<div>
<AddTodo />
<ul>
{todos.map(({ text }, index) => (
<TodoListItem key={index} text={text} />
))}
</ul>
</div>
)
}

This is good, but you still can't add a new to-do to your list. To do that, you need to handle events.

The fallacy of state

React provides simple primitives to manage state. The state of your application is simple to start but will grow more complex. A more complex state is harder to manage.

Managing state is going to be the root of all your problems. React props are easy; they are immutable and don't change. If your application was only properties it would be very easy to reason about. It also would not be very interesting. Nothing would change. State brings power, but that power brings complexity.

Events

Events are how your application can respond to incoming messages. An event may be a keypress, a button click, or incoming data. These events are handled with event handlers or functions that you define to handle the event. React will pass the event to your handler and you can handle it.

You've already used an event; the onChange event above on the input field. Every time the input changes, which happens when the user types, the onChange event triggers.

;(e) => setText(e.target.value)

Your simple handler takes the event, gets the target, and gets the value of that target. The target is where the event came from. In this case, that is the input field itself. And the value, therefore, is the current value on that input field, or what the user just typed! You set that to your state with setText.

React manages the event itself; that's a React.Event but the target attribute points to a regular HTML input field, and you can read about the various attributes on MDN.

Adding the to-do

To tie this together, you want to add the to-do to your list when the user clicks the button. To handle button clicks, you want to implement an onClick handler.

const AddTodo = ({}: AddTodoProps) => {
const onClick = () => {
onAddTodo(text)
setText('')
}
// Rest of the code ...
}

When the user clicks Add, then you want to:

  1. Add the to-do to the list
  2. Set the input text to be blank again

Setting the text is easy, you already know how to set the text! You just call setText('') and that will set it on the next render.

This component doesn't have the list of to-dos though. The parent component does. How do you tell the parent “Hey, I have a to-do for you to add!”? With a custom event handler! Here, your event handler will function as a JavaScript callback. You'll pass the function into your component and wait for a to-do to be added. Then you'll call the function with that to-do!

const TodoList = ({}: TodoListProps) => {
const [todos, setTodos] = useState<Todo[]>([])
const onAddTodo = (text: string) => {
setTodos((todos) => [...todos, { text }])
}
return (
<div className="container">
<AddTodo />
<ul>
{todos.map(({ text }, index) => (
<TodoListItem key={index} text={text} />
))}
</ul>
</div>
)
}

You define a regular function named onAddTodo, which expects a string parameter passed. When you set the state, you want to use the previous state to set the new one. You want to add a to-do to your list. That's what [...todos, { text }] does. This uses a slightly different syntax for setState:

setTodos([...todos, { text }])
// vs
setTodos((todos) => [...todos, { text }])

The first uses the todos that are defined above, with useState. This version may be stale though. So to ensure you are using the most updated list of to-dos, you should use the second syntax. This passes the current state value to a function, and you reference that instead.

Wire it up

The only thing left to do is wire it up!

const AddTodo = ({ onAddTodo }: AddTodoProps) => {
const onClick = () => {
onAddTodo(text)
setText('')
}
}
<AddTodo onAddTodo={onAddTodo} />

You should try to keep the names the same if you can. Only change the names of handlers if you need to disambiguate.

It should now work! You should be able to add to-dos to your list.

Mastering state and events

Managing state and responding to events take your static site and turn it into a real web app. It brings it to life and lets you really use it. You should be able to take this app and bring it with you, writing down your to-dos.

React provides simple primitives to manage state, which will take you very far. You'll learn how even these simple hooks let you manage complex state throughout your application.

Be the best web developer you can be.

A weekly email on Next.js, React, TypeScript, Tailwind CSS, and web development.

No spam. Unsubscribe any time.