As a developer, I was building a React application for a client who needed to track inventory for their small business in Texas. The application needed to update prices, quantities, and product availability in real-time as users interacted with it.
The challenge was managing all this changing data efficiently. That’s when I realized the true power of React’s state management.
In this article, I’ll walk you through everything you need to know about state in React JS, from basic concepts to advanced techniques.
So let’s get in!
State in React
State in React is a JavaScript object that holds data that may change over time and affects how a component renders. Think of state as the “memory” of your component.
Unlike props, which are passed down from parent components, state is managed within the component itself. When state changes, React re-renders the component to reflect those changes.
Here’s a simple example of state in a component:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}Check out Override a Styled Component in React
Method 1: Use the useState Hook
The useState hook is the simplest way to add state to functional components in React. It was introduced in React 16.8 and has become the standard approach.
Here are the steps to use useState:
- Import the useState hook from React
- Call useState with an initial value
- Destructure the returned array to get the state value and setter function
- Use the setter function to update the state
Let’s create a simple form that uses useState:
import React, { useState } from 'react';
function SignupForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isSubmitted, setIsSubmitted] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
// Form validation logic here
setIsSubmitted(true);
};
return (
<div>
{isSubmitted ? (
<p>Thanks for signing up!</p>
) : (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit">Sign Up</button>
</form>
)}
</div>
);
}For multiple related state values, you can combine them into a single object:
const [formData, setFormData] = useState({
email: '',
password: '',
});
// Update using the spread operator
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};I executed the above example code and added the screenshot below.

Read Build a Reusable Icon Component in React
Method 2: Use the useReducer Hook
When your state logic becomes complex, useReducer it provides a more structured approach. It’s inspired by Redux and follows the reducer pattern.
This is especially useful for:
- Complex state logic with multiple sub-values
- The state that depends on the previous state
- Deep updates where you need to update nested properties
Here’s how to implement a shopping cart using useReducer:
import React, { useReducer } from 'react';
const initialState = {
items: [],
total: 0,
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price,
};
case 'REMOVE_ITEM':
const filteredItems = state.items.filter(
item => item.id !== action.payload.id
);
return {
...state,
items: filteredItems,
total: filteredItems.reduce((sum, item) => sum + item.price, 0),
};
default:
return state;
}
}
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, initialState);
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (itemId) => {
dispatch({ type: 'REMOVE_ITEM', payload: { id: itemId } });
};
return (
<div>
<h2>Shopping Cart</h2>
<p>Total: ${cart.total.toFixed(2)}</p>
<button onClick={() => addItem({ id: Date.now(), name: 'Product', price: 29.99 })}>
Add Sample Product
</button>
<ul>
{cart.items.map(item => (
<li key={item.id}>
{item.name} - ${item.price.toFixed(2)}
<button onClick={() => removeItem(item.id)}>Remove</button>
</li>
))}
</ul>
</div>
);
}I executed the above example code and added the screenshot below.

Read Props in React JS
Method 3: Use Context API for Global State
Sometimes you need to share state across many components. Props drilling (passing props through multiple levels) can become unwieldy. This is where Context API comes in.
Context provides a way to share values like state between components without explicitly passing props through every level of the component tree.
Here’s how to implement a theme switcher using Context:
import React, { createContext, useContext, useState } from 'react';
// Create a context
const ThemeContext = createContext();
// Create a provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook to use the theme context
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// A component that uses the theme
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
padding: '8px 16px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
>
Toggle Theme
</button>
);
}
// App component that uses the provider
function App() {
return (
<ThemeProvider>
<div style={{ padding: '20px' }}>
<h1>Theme Switcher Example</h1>
<ThemedButton />
</div>
</ThemeProvider>
);
}I executed the above example code and added the screenshot below.

Read Form Validation in React.js
State Management Best Practices
After working with React state for years, I’ve developed some best practices that help keep my code clean and maintainable:
- Keep state as local as possible: Only lift state when necessary. The closer the state is to where it’s used, the simpler your app will be.
- Use the right tool for the job:
useStatefor a simple stateuseReducerfor complex state logic- Context API for global state
- Consider external libraries like Redux or Zustand for very complex applications
- Separate business logic from UI logic: Keep your component functions focused on rendering, and move complex logic to custom hooks or reducer functions.
- Avoid state duplication: Derive state when possible instead of keeping multiple copies that can get out of sync.
- Batch state updates: When making multiple state updates in response to a single event, consider using useReducer or batching updates.
Common State Management Mistakes to Avoid
Here are some pitfalls I’ve encountered and how to avoid them:
Directly Mutating State
// ❌ Wrong - Direct mutation
const handleClick = () => {
user.name = 'New Name';
setUser(user); // This won't trigger a re-render!
};
// ✅ Correct - Create a new object
const handleClick = () => {
setUser({ ...user, name: 'New Name' });
};Updating State Based on Previous State Incorrectly
// ❌ Wrong - Race condition risk
const increment = () => {
setCount(count + 1);
};
// ✅ Correct - Using functional update
const increment = () => {
setCount(prevCount => prevCount + 1);
};Overusing Global State
It’s tempting to put everything in global state, but this can lead to performance issues and make your app harder to maintain. Keep state as local as possible and only lift it up when necessary.
Read Pass Props to a Component in React
Advanced State Patterns
Once you’re comfortable with the basics, you can explore more advanced patterns:
State Machines with XState
For complex state transitions, consider using a state machine library like XState. This helps formalize your state logic and prevents impossible states.
Optimistic UI Updates
For a more responsive user experience, update your UI immediately before waiting for server responses, then reconcile if there are errors.
const addTodo = async (text) => {
// Create a temporary ID
const tempId = Date.now();
// Optimistically add the todo to state
setTodos(prev => [...prev, { id: tempId, text, status: 'pending' }]);
try {
// Make the actual API call
const response = await api.addTodo(text);
// Replace the temporary todo with the real one from the server
setTodos(prev => prev.map(todo =>
todo.id === tempId ? { ...response.data, status: 'completed' } : todo
));
} catch (error) {
// On error, either remove the todo or mark it as failed
setTodos(prev => prev.map(todo =>
todo.id === tempId ? { ...todo, status: 'failed' } : todo
));
// Show error to user
setError('Failed to add todo');
}
};I hope you found this article helpful. State management is at the heart of React development, and mastering it will significantly improve your ability to build complex, interactive applications. Whether you’re building a simple form or a complex dashboard for a Fortune 500 company, these principles will help you write cleaner, more maintainable code.
Related tutorials:

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.