React State Management
Updating State
By setting a static value
Using an updater function that takes the previous value
Updating State Based on Previous State
When a new state depends on the previous one, use the function form of setState()
.
Incorrect Example
function Counter() {
const [counter, setCounter] = useState(0);
function handleIncrement() {
setCounter(counter + 1);
// Not reliable if updates happen quickly
}
return (
<>
<p>Counter Value: {counter}</p>
<button onClick={handleIncrement}>Increment</button>
</>
);
}
Correct Approach
function Counter() {
const [counter, setCounter] = useState(0);
function handleIncrement() {
setCounter(prevCounter => prevCounter + 1);
}
return (
<>
<p>Counter Value: {counter}</p>
<button onClick={handleIncrement}>Increment</button>
</>
);
}
When you pass a function to setCounter
, React calls the function and passes the latest state value. The function should return a value for the new value to be stored by react.
Using an Updater Function
const [counter, setCounter] = useState(0);
<button onClick={() => setCounter((prev) => prev + 1)}>Increment</button>
This is useful when the next state depends on the previous state.
Changing the Type Dynamically
This is valid in React:
const [counter, setCounter] = useState(0);
setCounter("hi there");
Although this is legal, changing types dynamically can lead to bugs and confusion. Prefer keeping state types consistent.
Example: Resetting a Counter
Here’s an example of using state to implement a simple counter with an Increment and Reset button:
import { useState } from 'react';
function Counter() {
const [counter, setCounter] = useState(0);
return (
<main>
<p>Counter: {counter}</p>
<button onClick={() => setCounter(val => val + 1)}>
Increment
</button>
<button onClick={() => setCounter(0)}>
Reset
</button>
</main>
);
}
function App() {
return <Counter />;
}
export default App;
NOTE
React Developer Tools allow you to visually inspect re-renders and state changes.
Chrome/Edge: React DevTools
Firefox: React DevTools
Don’t Mutate State Directly
React compares new and old state values by reference. This means:
If you update state with the same object or array reference, React won't re-render.
Even if the contents of the object or array changed, React won’t detect it unless the reference itself changes.
const arr = [1, 2, 3];
arr.push(4); // Mutates the original array (bad)
setState(arr); // React may not re-render
Instead, create a new reference:
setState(prev => [...prev, 4]); // new array reference
Example: Updating an Array in State (Todo App)
Here’s a correct way to remove an item from an array in state:
import { useState } from "react";
function TodoApplication({ initialList }) {
const [todos, setTodos] = useState(initialList);
return (
<main>
{todos.map((todo, index) => (
<p key={todo}>
{todo}
<button
onClick={() =>
setTodos(prev => [
...prev.slice(0, index),
...prev.slice(index + 1),
])
}
>x</button>
</p>
))}
</main>
);
}
function App() {
const items = ["Feed the plants", "Water the dishes", "Clean the cat"];
return <TodoApplication initialList={items} />;
}
export default App;
This creates a new array that omits the deleted item and passes that to setTodos
.
Setting Function to a state
import { useState } from "react";
const PLUS = (a,b) => a+b;
const MINUS = (a,b) => a-b;
const MULTIPLY = (a,b)=> a*b;
function Calculator({a, b})
{
const [operator, setOperator] = useState( ()=> PLUS );
return (
<main>
<h1>Calculator</h1>
<button onClick= { ()=> setOperator( ()=>PLUS) } >
Plus
</button>
<button onClick= { ()=> setOperator( ()=>MINUS) } >
Minus
</button>
<button onClick= { ()=> setOperator( ()=>MULTIPLY) } >
Multiply
</button>
<p>
Result of applying operator to {a} and {b}:
<code>{operator(a,b)}</code>
</p>
</main>
);
}
function App() {
return <Calculator a={7} b={4} />
}
export default App;
The state value can be called now as a function to take the variables.
Using Multiple States in a Component
Call useState()
for each separate piece of state:
function LoginForm() {
const [enteredEmail, setEnteredEmail] = useState('');
const [enteredPassword, setEnteredPassword] = useState('');
function handleUpdateEmail(event) {
setEnteredEmail(event.target.value);
}
function handleUpdatePassword(event) {
setEnteredPassword(event.target.value);
}
return (
<form>
<input
type="email"
placeholder="Your email"
onBlur={handleUpdateEmail}
/>
<input
type="password"
placeholder="Your password"
onBlur={handleUpdatePassword}
/>
</form>
);
}
Example: Todo App with Filtering and Completion
We’ll extend our todo app to include:
A toggle to show/hide completed tasks.
A way to mark tasks as done.
Two separate state values: one for todos, one for the filter.
import { useState } from "react";
function markDone(list, index) {
return list.map((item, i) =>
i === index ? { ...item, done: true } : item
);
}
function TodoApplication({ initialList }) {
const [todos, setTodos] = useState(initialList);
const [hideDone, setHideDone] = useState(false);
const filteredTodos = hideDone
? todos.filter(({ done }) => !done)
: todos;
return (
<main>
<div style={{ display: "flex", gap: "10px" }}>
<button onClick={() => setHideDone(false)}>Show All</button>
<button onClick={() => setHideDone(true)}>Hide Done</button>
</div>
{filteredTodos.map((todo, index) => (
<p key={todo.task}>
{todo.done ? (
<strike>{todo.task}</strike>
) : (
<>
{todo.task}
<button
onClick={() =>
setTodos(prev => markDone(prev, todo.index))
}
>✓</button>
</>
)}
</p>
))}
</main>
);
}
function App() {
const items = [
{ task: "Feed the plants", done: false, index: 0 },
{ task: "Water the dishes", done: false, index: 1 },
{ task: "Clean the cat", done: false, index: 2 },
];
return <TodoApplication initialList={items} />;
}
export default App;
Multiple States with Objects / Merged State Object
Use a single object to manage all related state:
function LoginForm() {
const [userData, setUserData] = useState({
email: '',
password: ''
});
function handleUpdateEmail(event) {
setUserData({
email: event.target.value,
password: userData.password
});
}
function handleUpdatePassword(event) {
setUserData({
email: userData.email,
password: event.target.value
});
}
// return form...
}
function LoginForm() {
const [userData, setUserData] = useState({
email: '',
password: ''
});
function handleUpdateEmail(event) {
setUserData(prevData => ({
...prevData,
email: event.target.value
}));
}
function handleUpdatePassword(event) {
setUserData(prevData => ({
...prevData,
password: event.target.value
}));
}
}
IMPORTANT
Always use the function form to the state-updating function if the new state depends on the previous state. If it depends on some other value (user input), directly passing the new state value as a function argument is absolutely fine and recommended.
IMPORTANT
When updating state objects, you must preserve unchanged properties manually. React replaces the old object entirely with the new one. The old ones will be lost.
Incorrect:
setUserData({ email: 'test@example.com' }); // password is lost
Correct:
setUserData({
email: 'test@example.com',
password: userData.password
});
Two-Way Binding
Two-way binding means user input updates state, and state controls the input value.
Input source (typically an <input>
element) sets some state upon user input (upon the change event) and outputs the input at the same time.
function NewsletterField() {
const [email, setEmail] = useState('');
function handleUpdateEmail(event) {
setEmail(event.target.value);
}
return (
<input
type="email"
placeholder="Your email address"
value={email}
onChange={handleUpdateEmail}
/>
);
}
React prevents infinite loops and keeps input and state in sync.
NOTE
This might look like an infinite loop, but React deals with this. This is referred to as two-way binding as a value is both set and read from the same source.
Resetting Input
function NewsletterField() {
const [email, setEmail] = useState('');
function handleClearInput() {
setEmail('');
}
return (
<>
<input
type="email"
placeholder="Your email address"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<button onClick={handleClearInput}>Reset</button>
</>
);
}
Without two-way binding, clearing the state wouldn't reset the input field.
Deriving Values from State
You can derive values from state directly in the component. Passing state via props to repeat what was typed.
Simple Echo
function Repeater() {
const [userInput, setUserInput] = useState('');
// function handleChange(event) {
// setUserInput(event.target.value); };
return (
<>
<input
type="text"
onChange={
e => setUserInput(e.target.value)}
/>
<p>You entered: {userInput}</p>
</>
);
}
Derived Value (Character Count)
function Repeater() {
const [userInput, setUserInput] = useState('');
const numChars = userInput.length;
return (
<>
<input
type="text"
onChange={
e => setUserInput(e.target.value)}
/>
<p>Characters entered: {numChars}</p>
</>
);
}
No need to store derived values in state—they can be calculated on render.
Forms and Form Submission
Use onSubmit
on <form>
to assign a function and call event.preventDefault()
to ensure browse won't generate HTTP request by default but manage form submissions manually.
function NewsletterSignup() {
const [email, setEmail] = useState('');
const [agreed, setAgreed] = useState(false);
function handleUpdateEmail(event) {
// could add email validation here
setEmail(event.target.value);
};
function handleUpdateAgreement(event) {
setAgreed(event.target.checked);
// checked is a default JS boolean property
};
function handleSignup(event) {
event.preventDefault();
// prevent browser default of sending a Http request
const userData = {userEmail: email, userAgrees: agreed};
// doWhateverYouWant(userData);
};
return (
<form onSubmit={handleSignup}>
<div>
<label htmlFor="email">Your email</label>
<input type="email" id="email" onChange={handleUpdateEmail}/>
</div>
<div>
<input type="checkbox" id="agree" onChange={handleUpdateAgreement}/>
<label htmlFor="agree">Agree to terms and conditions</label>
</div>
</form>
);
};
Shorter Version with function defined in onChange
directly.
function NewsletterSignup() {
const [email, setEmail] = useState('');
const [agreed, setAgreed] = useState(false);
function handleSignup(event) {
event.preventDefault();
const userData = { userEmail: email, userAgrees: agreed };
// handle submission logic
}
return (
<form onSubmit={handleSignup}>
<div>
<label htmlFor="email">Your email</label>
<input type="email" id="email" onChange={e => setEmail(e.target.value)} />
</div>
<div>
<input type="checkbox" id="agree" onChange={e => setAgreed(e.target.checked)} />
<label htmlFor="agree">Agree to terms and conditions</label>
</div>
<button type="submit">Sign Up</button>
</form>
);
}
Note: Use
htmlFor
instead offor
in JSX to avoid conflicts with reserved keywords which represents JS loop.
Lifting State Up
To manage state across multiple components, you can "lift state up" to a parent component and pass both the state and its setter as props.
Incorrect: State in Child Only
function SearchBar() {
const [searchTerm, setSearchTerm] = useState('');
return <input
type="search"
onChange={e => setSearchTerm(e.target.value)}
/>;
}
function Overview() {
return <p>
Currently searching for {searchTerm}
</p>; // Not accessible here
}
Correct: Lift State to Parent
function App() {
const [searchTerm, setSearchTerm] = useState('');
return (
<>
<SearchBar
onUpdateSearch={e => setSearchTerm(e.target.value)} />
<Overview currentTerm={searchTerm} />
</>
);
}
function SearchBar({ onUpdateSearch }) {
return <input
type="search"
onChange={onUpdateSearch} />;
}
function Overview({ currentTerm }) {
return <p>Currently searching for {currentTerm}</p>;
}
Changes in App
cause re-rendering of child components, keeping the UI in sync.
IMPORTANT
State is lifted by using props in the components that need to manipulate (that is, set) or read state, and by registering state in the ancestor component that is shared by the two other components.
Full Example:
We’ll now split our TodoApplication
into smaller components:
FilterButton
: Renders the filter control.Task
: Renders each task.TodoApplication
: Holds state and logic.
import { useState } from "react";
function markDone(list, index) {
return list.map((item, i) =>
i === index ? { ...item, done: true } : item
);
}
function FilterButton({ current, flag, setFilter, children }) {
const style = {
border: "1px solid dimgray",
background: current === flag ? "dimgray" : "transparent",
color: current === flag ? "white" : "dimgray",
padding: "4px 10px",
marginRight: "10px",
};
return (
<button style={style} onClick={() => setFilter(flag)}>
{children}
</button>
);
}
function Task({ task, done, markDone }) {
const paragraphStyle = {
color: done ? "gray" : "black",
display: "flex",
alignItems: "center",
};
const buttonStyle = {
border: "none",
background: "transparent",
color: "inherit",
cursor: "pointer",
marginRight: "8px",
};
return (
<p style={paragraphStyle}>
<button style={buttonStyle} onClick={done ? null : markDone}>
{done ? "✓" : "◯"}
</button>
{done ? <strike>{task}</strike> : task}
</p>
);
}
function TodoApplication({ initialList }) {
const [todos, setTodos] = useState(initialList);
const [hideDone, setHideDone] = useState(false);
const filteredTodos = hideDone
? todos.filter(({ done }) => !done)
: todos;
return (
<main>
<div style={{ display: "flex", marginBottom: "10px" }}>
<FilterButton
current={hideDone}
flag={false}
setFilter={setHideDone}
>
Show All
</FilterButton>
<FilterButton
current={hideDone}
flag={true}
setFilter={setHideDone}
>
Hide Done
</FilterButton>
</div>
{filteredTodos.map((todo, index) => (
<Task
key={todo.task}
task={todo.task}
done={todo.done}
markDone={() =>
setTodos(prev => markDone(prev, todo.index))
}
/>
))}
</main>
);
}
function App() {
const items = [
{ task: "Feed the plants", done: false, index: 0 },
{ task: "Water the dishes", done: false, index: 1 },
{ task: "Clean the cat", done: false, index: 2 },
];
return <TodoApplication initialList={items} />;
}
export default App;
Summary and Key Takeaways
Event handlers are added via
onEventName
props (e.g.,onClick
,onChange
).Use
useState()
to define and manage internal component state.useState()
returns[currentValue, setValue]
.Use a function form of the state updater if the new value depends on the previous one.
Any JavaScript value (string, number, object, array) can be stored in state.
For inputs, use two-way binding (
value
+onChange
) to keep state and UI in sync.Handle form submissions using
onSubmit
+preventDefault()
.When multiple components need access to the same state, lift state up to their common ancestor.