State (useState)
In this chapter, you learn about State, which manages a component's state.
What you'll learn in this chapter
- What State is and why you need it
- How to use the useState hook
- Updating array and object State
- The difference between State and Props
This chapter uses the project you created in Chapter 3. Start the development server with npm run dev and study while writing code.
The code examples in this chapter can be copied directly into App.tsx and run.
What is State
State is mutable data managed inside a component. When State is updated, the component re-renders.
Why do you need State? With a regular variable, even if you change the value, React can't detect the change and the screen doesn't update. By using State, you can tell React about the change in value and update the UI automatically.
The useState hook
In React, you use the useState hook to manage State.
When you call useState, two things are returned together:
- The current value (for reading) — gets the current state
- The update function (for writing) — changes the state and re-renders the screen
const [count, setCount] = useState(0)
// ↑read ↑write ↑initial value
import { useState } from 'react'
import './App.css'
function Counter() {
// useState returns the array [current value, update function]
// Assign to count and setCount with array destructuring
const [count, setCount] = useState(0)
// The function called on a button click
const increment = () => {
// prev receives the current value of count
// The result of prev + 1 becomes the new count
setCount(prev => prev + 1)
}
return (
<div>
{/* When the value of count changes, this part updates automatically */}
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
</div>
)
}
export default function App() {
return <Counter />
}
You can pass a value directly to setCount, but you can also pass a function. When you pass a function, React runs it, passing the current value as the argument.
setCount(prev => prev + 1)
↑ ↑
│ └─ return value = the new state value
└─ React passes the current value (count) for you
The syntax of useState
useState returns an array. You receive this array with destructuring.
const [state, setState] = useState(initialValue)
| Element | Description | The earlier example |
|---|---|---|
state | The current state value | count |
setState | The function that updates the state | setCount |
initialValue | The initial value | 0 |
Naming convention
The update function name is the state name with set prefixed:
const [count, setCount] = useState(0) // counter
const [name, setName] = useState('') // name
const [isOpen, setIsOpen] = useState(false) // open/closed state
*Array destructuring is explained in Chapter 2.
Specifying types
In TypeScript, the type is inferred from the initial value.
// Type is inferred (the type is determined automatically from the initial value)
const [count, setCount] = useState(0) // inferred as number
const [name, setName] = useState('') // inferred as string
const [isActive, setIsActive] = useState(false) // inferred as boolean
// Specify the type explicitly (when the initial value alone doesn't give the intended type)
// <User | null> means "the User type or null"
const [user, setUser] = useState<User | null>(null)
// <string[]> means "an array of strings"
const [items, setItems] = useState<string[]>([])
The <User | null> in useState<User | null>(null) is syntax called generics. If the initial value is only null, it's inferred as the null type and you can't put a User object in later, so you explicitly state "the User type or null."
When the initial value's computation is expensive (lazy initialization)
When you pass a value directly to useState, that value is evaluated on every render. The initial value is used only on the first render, but if you pass a localStorage read or a heavy computation directly, it runs uselessly on every subsequent render too.
// JSON.parse runs on every render (wasteful)
const [items, setItems] = useState(JSON.parse(localStorage.getItem('items') ?? '[]'))
In such cases, by passing a function to useState, you can make that function run only on the first render. This is called lazy initialization.
// JSON.parse runs only on the first render
const [items, setItems] = useState(() => {
return JSON.parse(localStorage.getItem('items') ?? '[]')
})
Lazy initialization is effective when the initial value's computation is expensive (generating a large array, synchronous I/O, complex calculations). If you're passing a simple literal (0, '', false, and so on), there's no need to use lazy initialization.
Why does passing a function let it run "only the first time"?
There are two key points.
1. The initial value of useState is only used on the first render
The initial value you pass to useState is referenced only once, when the component is first displayed on the screen. On the second and subsequent re-renders, React uses the State value it holds internally, so the initial value is completely ignored.
2. JavaScript "computes a function's arguments first, then calls the function"
JavaScript has a rule that "when calling a function, it evaluates the contents of the parentheses (the arguments) first, then passes them to the function." This is the cause of the pitfall.
❌ When you pass the value directly
useState(JSON.parse(localStorage.getItem('items') ?? '[]'))
This code runs in the following order on every render.
- Get the data with
localStorage.getItem - Parse the string with
JSON.parse - Pass the result to
useState
In other words, the heavy processing is already complete before useState is called. There's no room for React to decide "this initial value is no longer needed." As a result, JSON.parse keeps running uselessly on the second and subsequent renders too.
⭕ When you pass a function
useState(() => JSON.parse(localStorage.getItem('items') ?? '[]'))
In this code, what you pass to useState is the function itself. A function "doesn't run its contents just by being defined," so what's passed to useState is "a procedure" that hasn't been computed yet.
And React behaves as follows.
- First render: the initial value is needed → run the received function to get the result of
JSON.parse - Second and later: State already exists, so the initial value is unnecessary → discard the received function without running it
This technique of "not running the computation right away, but passing it in a form that can be run only when needed" is called lazy initialization.
Updating State
Specifying a value directly
setCount(10)
setName('Tanaka')
setIsActive(true)
Updating based on the previous value
When updating by referring to the previous value, pass a function.
// Recommended: use a function
setCount(prev => prev + 1)
// Not recommended: directly reference the current value
setCount(count + 1)
This prev is an argument that receives "the current State value." When you pass a function to setCount, React runs it, passing the current value. The variable name is up to you, but prev (previous) is commonly used.
A State variable like count is fixed to its value at that point in the render. No matter how many times you call setCount inside the same event handler, the value of the variable count doesn't change until the render ends. So when you want to compute based on the result of the immediately preceding update, you need to use the function form. Updates passed in the function form are queued (added to a list of pending operations), and React runs them in order, which lets you carry the result of the immediately preceding update into the next computation.
// Problematic code
const handleTripleIncrement = () => {
setCount(count + 1) // references count = 0 → schedules 1
setCount(count + 1) // count is still 0 in the same render → schedules 1
setCount(count + 1) // count is still 0 in the same render → schedules 1
// All three schedule "0 + 1 = 1", so the final result is 1 (count is a fixed value within the render)
}
// Correct code
const handleTripleIncrement = () => {
setCount(prev => prev + 1) // queue: 0 → 1
setCount(prev => prev + 1) // queue: 1 → 2
setCount(prev => prev + 1) // queue: 2 → 3
// Result: 3
}
React processes multiple State updates that occur within a single event handler together (batching) and re-renders just once at the end. This has been effective inside asynchronous processing as well since React 18 (automatic batching). So even if you read State again inside the handler, you can't get the updated value. When you want to use the updated value, use the function-form update function, or use the new value you receive on the next render.
Let's try it: create a counter
Create a counter component with +1, -1, and reset buttons.
Hint
- Create a State with initial value 0 using
useState(0) - Update to
+1,-1, and0respectively with three buttons - Update +1 and -1 in the function form, like
prev => prev + 1
Answer and explanation
import { useState } from 'react'
import './App.css'
function Counter() {
const [count, setCount] = useState(0)
// +1 based on the previous value (function form)
const increment = () => {
setCount(prev => prev + 1)
}
// -1 based on the previous value (function form)
const decrement = () => {
setCount(prev => prev - 1)
}
// Simply set to 0 (specify the value directly)
const reset = () => {
setCount(0)
}
return (
<div>
<p>Count: {count}</p>
{/* Set the handler corresponding to each button */}
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
)
}
export default function App() {
return <Counter />
}
You define event handlers as functions and pass them to onClick. Updating in the function form like setCount(prev => prev + 1) always lets you update based on the latest value. Reset simply sets 0, so it doesn't need to be in the function form.
Object State
When handling an object as State, you create a new object to update it. *Spread syntax is explained in Chapter 2.
For object or array State, don't mutate it directly; always create a new object. React decides whether to re-render by comparing the references (the memory addresses) of "the previous value" and "the new value." If you mutate directly, the reference stays the same, so React can't detect the change and the screen doesn't update. This is the "immutable update" principle explained in Chapter 2.
import { useState } from 'react'
import './App.css'
// Define the type of the object used in State
type User = {
name: string
age: number
}
function UserForm() {
// Set an object as the initial value
// Specify the type explicitly with <User> (inference is also possible)
const [user, setUser] = useState<User>({ name: '', age: 0 })
// e: the event object (contains information about the input element)
// React.ChangeEvent<HTMLInputElement> is the type of an input element's change event
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// When returning an object from an arrow function, wrap it in ()
// { ...prev } copies the existing properties, and only name is overwritten
setUser(prev => ({ ...prev, name: e.target.value }))
}
const handleAgeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// e.target.value is always a string, so convert it to a number with Number()
setUser(prev => ({ ...prev, age: Number(e.target.value) }))
}
return (
<div>
{/* Create a "controlled component" with value and onChange */}
{/* React manages the input value and keeps it in sync with State */}
<input
value={user.name}
onChange={handleNameChange}
placeholder="Name"
/>
<input
type="number"
value={user.age}
onChange={handleAgeChange}
placeholder="Age"
/>
<p>{user.name} ({user.age} years old)</p>
</div>
)
}
export default function App() {
return <UserForm />
}
For the event handler's argument e, we specify the type React.ChangeEvent<HTMLInputElement>. This lets you access e.target.value in a type-safe way.
Let's try it: create an input form
Create a component that displays "Hello, 〇〇!" in real time as you type a name.
Hint
- Set an empty-string initial value with
useState('') - Bind the State to the
valueof<input> - Reflect the input value into State with
onChange - Display it only when there's a name with
{name && <p>...</p>}
Answer and explanation
import { useState } from 'react'
import './App.css'
function GreetingInput() {
// Initialize with an empty string (inferred as string)
const [name, setName] = useState('')
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// Get the entered string with e.target.value
setName(e.target.value)
}
return (
<div>
{/* value={name} displays the State value */}
{/* onChange={...} updates State on every input */}
<input
type="text"
value={name}
onChange={handleNameChange}
placeholder="Enter a name"
/>
{/* name && ... means "if name is not empty, display ..." */}
{/* An empty string is treated as false, so it's displayed only when there's input */}
{name && <p>Hello, {name}!</p>}
</div>
)
}
export default function App() {
return <GreetingInput />
}
You define event handlers as functions and pass them to onChange. Combining value and onChange creates a "controlled component." With {name && ...}, the greeting is displayed only when a name has been entered.
Array State
You also create a new array to update an array.
import { useState } from 'react'
import './App.css'
function TodoList() {
// Specify the type "array of strings" with <string[]>
const [todos, setTodos] = useState<string[]>([])
const [input, setInput] = useState('')
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value)
}
const addTodo = () => {
// Remove leading/trailing whitespace with trim() and check it's not empty
if (input.trim()) {
// [...prev, input] creates a new array by adding a new element to the existing array
setTodos(prev => [...prev, input])
// After adding, clear the input field
setInput('')
}
}
// Create a handler that takes an argument with a "function that returns a function"
const createRemoveHandler = (index: number) => () => {
// With filter, keep only the elements other than the specified index
// _ is a conventional way of writing an "unused variable"
setTodos(prev => prev.filter((_, i) => i !== index))
}
return (
<div>
<input
value={input}
onChange={handleInputChange}
/>
<button onClick={addTodo}>Add</button>
<ul>
{/* With map, transform each element of the array into a list item */}
{todos.map((todo, index) => (
// key is needed to identify each element of the list
<li key={index}>
{todo}
{/* createRemoveHandler(index) returns a function, so you can pass it to onClick */}
<button onClick={createRemoveHandler(index)}>Delete</button>
</li>
))}
</ul>
</div>
)
}
export default function App() {
return <TodoList />
}
This example uses key={index} for simplicity, but in a list that has additions, deletions, or reordering of elements, using the index as the key becomes a source of bugs. In the next exercise, you learn the correct method of using a unique ID.
Let's try it: create a Todo list
Referring to the TodoList component above, add a feature to toggle completed/incomplete.
Hint
- Define the type with
type Todo = { id: number; text: string; completed: boolean } - Generate a unique ID with
Date.now() - Operate the completed state with
<input type="checkbox" checked={...} onChange={...} /> - Transform the array with
mapand flip thecompletedof the matching ID - Show a strikethrough with
textDecoration: 'line-through'
Answer and explanation
import { useState } from 'react'
import './App.css'
// Define the type of each Todo
type Todo = {
id: number // unique identifier
text: string // the task's text
completed: boolean // completed state
}
function TodoList() {
// Specify the type "array of Todo objects" with <Todo[]>
const [todos, setTodos] = useState<Todo[]>([])
const [input, setInput] = useState('')
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value)
}
const addTodo = () => {
if (input.trim()) {
setTodos(prev => [
...prev,
{
// Date.now() returns the current time in milliseconds (used as a unique ID)
id: Date.now(),
text: input,
completed: false
}
])
setInput('')
}
}
// The function that toggles the completed state
const toggleTodo = (id: number) => {
setTodos(prev =>
// Transform the array with map (change only the matching element)
prev.map(todo =>
// If it's the matching ID, flip completed; otherwise leave it as-is
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
)
}
// The function that deletes
const removeTodo = (id: number) => {
// With filter, keep only the elements other than the matching ID
setTodos(prev => prev.filter(todo => todo.id !== id))
}
return (
<div>
<input
value={input}
onChange={handleInputChange}
placeholder="New task"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
// Use a unique ID for key, not the index (best practice)
<li key={todo.id}>
{/* Wrapping in label lets clicking the text also toggle the check */}
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{/* Represent the completed state visually with a checkbox */}
<input
type="checkbox"
checked={todo.completed}
// Call toggleTodo when the check changes via onChange
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
// When completed, distinguish visually with a strikethrough and gray color
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#999' : 'inherit'
}}>
{todo.text}
</span>
</label>
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
)
}
export default function App() {
return <TodoList />
}
Bind todo.completed to the checkbox's checked attribute and toggle the state with onChange. By wrapping in <label>, clicking the text also toggles the check, improving usability. Transform the array with map and flip only the matching element's completed. With the object spread syntax { ...todo, completed: !todo.completed }, you create a new object without modifying the original. It's best practice to use a unique ID for key, not the index.
Multiple States
A single component can have multiple States.
function Form() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
// ...
}
State vs Props
| Item | State | Props |
|---|---|---|
| Where managed | Inside the component | The parent component |
| Mutable | Yes (setState) | No (read-only) |
| Purpose | Managing internal state | Passing data |
Lifting state up
When you want to share the same State across multiple components, move the State to a common parent component.
How re-rendering works
When State is updated, that component and its child components re-render.
Parent (state changes) → re-render
├─ Child1 → re-render
└─ Child2 → re-render
How to prevent unnecessary re-renders is covered in Chapter 22 (performance optimization).
import { useState } from 'react'
import './App.css'
// A display-only component (has no State, receives the value via Props)
function Display({ count }: { count: number }) {
return <p>Count: {count}</p>
}
// An operation-only component (has no State, receives a function via Props)
// onIncrement: () => void is the type "a function with no arguments and no return value"
function Controller({ onIncrement }: { onIncrement: () => void }) {
return <button onClick={onIncrement}>+1</button>
}
// Manage State in the parent component (lifting up)
function Parent() {
// State you want to share is managed in the parent
const [count, setCount] = useState(0)
const handleIncrement = () => {
setCount(prev => prev + 1)
}
return (
<div>
{/* Pass State and the update function to the children via Props */}
<Display count={count} />
<Controller onIncrement={handleIncrement} />
</div>
)
}
export default function App() {
return <Parent />
}
Summary
- State is mutable data inside a component
- Manage State with the
useStatehook - Use the
setStatefunction to update it - When updating based on the previous value, use the function form
- Create a new reference to update objects and arrays
- Lift shared State up to the parent
In the next chapter, you learn about event handling.