
React State Management - Part 1: Getting Started with Built-in Tools
July 21, 2025
Ashish Gogula

July 21, 2025
Ashish Gogula
If you've been working with React for a bit, you've probably heard the word "state" tossed around a lot. And if you're anything like I was early on, you might be wondering:
Why is everyone so obsessed with state?

Well, state is what makes your app come alive. It’s what lets your UI respond when a user types something, clicks a button, or receives data from an API. In short: no state, no interactivity.
In this post, we’ll talk about:
Think of state as the memory of your app. It's any data that can change over time and influences what gets rendered.
A few types of state:
Managing state isn’t hard at first… but once your app grows, it can turn into spaghetti real quick
Before you reach for Redux or Zustand, it’s good to know that React already gives you some pretty solid tools.
Let’s go over the big three:
useState - The OG
// Step 1: Import the useState hook from React
import { useState } from "react";
// Step 2: Create a simple Counter component
function Counter() {
// Step 3: Declare a state variable 'count' with initial value 0
// 'setCount' is the function used to update 'count'
const [count, setCount] = useState(0);
// Step 4: Render a button that shows the current count
// When clicked, it increments the count by 1 using setCount
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Use it when:
You need a small piece of state that only lives in one component — like toggling a menu or tracking form input.
Sometimes, your state logic gets complicated. Like, “I have 4 different actions and need to update nested fields” complicated.
That’s where useReducer comes in.
import { useReducer } from "react";
// Step 1: Define your reducer function
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
// Step 2: Initialize state using useReducer
const [state, dispatch] = useReducer(reducer, { count: 0 });
// Step 3: Use state and dispatch in your JSX
return (
<div>
<h2>Count: {state.count}</h2>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</div>
);
}
Use it when:
You’ve probably run into prop drilling — passing the same value through 3... 4... 5 components just to get it where it’s needed. 😩
Context API helps fix that.
// Step 1: Import necessary hooks and createContext from React
import React, { createContext, useContext, useState } from "react";
// Step 2: Create a new context (can be placed in a separate file for larger apps)
const ThemeContext = createContext();
// Step 3: Create a provider component that wraps your app or part of it
function App() {
// Declare a piece of state to hold the current theme
const [theme, setTheme] = useState("light");
// Provide the theme value and updater function to children
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Page />
</ThemeContext.Provider>
);
}
// Step 4: A nested component that needs access to the theme
function Page() {
// Use useContext to access the context value
const { theme, setTheme } = useContext(ThemeContext);
// Render something based on the current theme
return (
<div className={theme}>
<h2>Current Theme: {theme}</h2>
<button onClick={() => setTheme("dark")}>Switch to Dark</button>
<button onClick={() => setTheme("light")}>Switch to Light</button>
</div>
);
}
Use it when:
Heads up:
Context re-renders all consumers whenever the value changes. So it’s not ideal for high-frequency updates like input values or mouse positions.
Here’s where it gets real. As your app grows, state management becomes less about what you want and more about managing complexity.
Some common pain points:
Prop Drilling
Too many props passed through too many layers = confusion + boilerplate
Shared State
How do two components on totally different branches of the tree talk to each other?
Async Data
Fetching stuff from APIs and juggling loading states, success, and errors can be a headache.
Performance
Too many unnecessary re-renders = slow app = unhappy users
Honestly? A lot of the time.
Here’s a quick rule of thumb:

Up Next: Part 2 — Client-Side State Libraries (Redux, Zustand & more)
We’ll dive into the most popular libraries for managing global state, compare them, and help you choose the right one for your app.