Explore the essentials of state management in React, from useState and useReducer to modern state libraries, with clear examples and best practices.
Table of content
State management is at the heart of every React application. Whether you’re building a simple todo list or a complex enterprise UI, understanding how state works in React is essential for writing bug-free, maintainable code. In this article, you’ll learn the fundamentals of managing state in React, compare built-in tools like useState
and useReducer
, and explore when to consider external libraries.
State represents any data that can change over time in your app’s UI. This includes things like user input, fetched data, toggled elements, and more. State helps React components respond and re-render appropriately to user actions.
useState
.useState
for Local StateThe useState
hook is React’s simplest way to create and update state in functional components.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // Initialize count to 0
return (
<div>
Count: {count}
<button onClick={() => setCount(count + 1)}>Increase</button>
</div>
);
}
export default Counter;
useReducer
useReducer
is better for complex state logic or multiple related values, helping you manage state transitions more predictably.
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
default: throw new Error(); // Handle unexpected actions
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 }); // Initial state
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
When two or more components need to share state, lift the state up to their nearest common ancestor and pass it down via props:
function Parent() {
const [shared, setShared] = useState('data');
return (
<>
<Child shared={shared} setShared={setShared} />
{/* Other components that need access to shared state */}
</>
);
}
function Child({ shared, setShared }) {
return (
<div>
<p>Shared Data: {shared}</p>
<button onClick={() => setShared('updated')}>Update Shared Data</button>
</div>
);
}
The built-in Context API
lets you create global state accessible throughout your component tree—no prop drilling required.
import React, { useContext, useState } from 'react';
const MyContext = React.createContext(); // Create a context
function Provider({ children }) {
const [value, setValue] = useState('shared'); // Initial state
return (
<MyContext.Provider value={{ value, setValue }}> {/* Provide the context */}
{children}
</MyContext.Provider>
);
}
function Consumer() {
const { value, setValue } = useContext(MyContext); // Consume the context
return (
<div>
<p>Value: {value}</p>
<button onClick={() => setValue('updated')}>Update Value</button>
</div>
);
}
For very large apps with deeply nested components, libraries like Redux, Zustand, or Recoil provide features like middleware, selectors, and time-travel debugging. Use them if:
useReducer
or Context.useState
and keep state as local as possible.Understanding and choosing the right state management approach keeps your React application scalable and clear. Master the fundamentals first, and scale up your tools only as your project demands. For more insights on React and web development, stay tuned on fulldev.pl!