Appearance
Managing State
FreshAs your application grows, it helps to be more intentional about how state is organized and how data flows between components.
Reacting to Input with State
Think of UI declaratively: describe what the UI should look like for each visual state, rather than imperatively manipulating the DOM.
flowchart LR
A[Empty] --> B[Typing]
B --> C[Submitting]
C --> D[Success]
C --> E[Error]
E --> Bjsx
import { useState } from 'react';
export default function Form() {
const [status, setStatus] = useState('typing'); // 'typing' | 'submitting' | 'success'
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
async function handleSubmit(e) {
e.preventDefault();
setStatus('submitting');
try {
await submitForm(answer);
setStatus('success');
} catch (err) {
setStatus('typing');
setError(err);
}
}
if (status === 'success') {
return <h1>Correct!</h1>;
}
return (
<form onSubmit={handleSubmit}>
<textarea
value={answer}
onChange={e => setAnswer(e.target.value)}
disabled={status === 'submitting'}
/>
<button disabled={answer.length === 0 || status === 'submitting'}>
Submit
</button>
{error !== null && <p className="Error">{error.message}</p>}
</form>
);
}Choosing the State Structure
Five principles for structuring state:
- Group related state. If two state variables always change together, unify them.
- Avoid contradictions. Don't have
isSendingandisSentboth betrue. - Avoid redundant state. If you can compute it from props or other state, don't store it.
- Avoid duplication. Don't store the same data in multiple state variables.
- Avoid deeply nested state. Flat/normalized state is easier to update.
jsx
// BAD: redundant state
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // redundant!
// GOOD: compute derived values
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName; // computedSharing State Between Components
Lift state up to the closest common parent:
jsx
import { useState } from 'react';
function Panel({ title, children, isActive, onShow }) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? <p>{children}</p> : <button onClick={onShow}>Show</button>}
</section>
);
}
export default function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<Panel title="About" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)}>
Content for About panel.
</Panel>
<Panel title="Etymology" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)}>
Content for Etymology panel.
</Panel>
</>
);
}Preserving and Resetting State
React preserves state for components that are rendered at the same position in the UI tree:
jsx
// Same component at same position = state preserved
{isFancy ? <Counter isFancy={true} /> : <Counter isFancy={false} />}
// Different components at same position = state destroyed
{isPaused ? <p>See you later!</p> : <Counter />}To reset state at the same position, give it a different key:
jsx
{isPlayerA
? <Counter key="Taylor" person="Taylor" />
: <Counter key="Sarah" person="Sarah" />
}Key Takeaway
Same component + same position = preserved state. Different key = reset state. React sees position in the tree, not in JSX.
Extracting State Logic into a Reducer
For complex state logic, extract it into a reducer function:
jsx
import { useReducer } from 'react';
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added':
return [...tasks, { id: action.id, text: action.text, done: false }];
case 'changed':
return tasks.map(t => t.id === action.task.id ? action.task : t);
case 'deleted':
return tasks.filter(t => t.id !== action.id);
default:
throw Error('Unknown action: ' + action.type);
}
}
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
function handleAddTask(text) {
dispatch({ type: 'added', id: nextId++, text });
}
function handleChangeTask(task) {
dispatch({ type: 'changed', task });
}
function handleDeleteTask(taskId) {
dispatch({ type: 'deleted', id: taskId });
}
return (
<>
<AddTask onAddTask={handleAddTask} />
<TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} />
</>
);
}useState vs useReducer
useState | useReducer | |
|---|---|---|
| Code size | Less upfront | More upfront (reducer + dispatch) |
| Readability | Simple updates are clear | Complex updates are clearer |
| Debugging | Harder to trace | Add console.log in reducer |
| Testing | N/A | Reducer is a pure function, easy to test |
Passing Data Deeply with Context
Context lets a parent provide data to the entire tree below it without passing props:
jsx
import { createContext, useContext, useState } from 'react';
const LevelContext = createContext(1);
function Heading({ children }) {
const level = useContext(LevelContext);
const Tag = 'h' + level;
return <Tag>{children}</Tag>;
}
function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}Context Use Cases
- Theming (dark mode)
- Current account / logged-in user
- Routing (most routers use context internally)
- Managing complex state (often combined with a reducer)
Scaling Up with Reducer and Context
Combine useReducer with Context to manage state across the tree:
jsx
import { createContext, useContext, useReducer } from 'react';
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}This pattern moves all state logic into a single file and keeps components clean.