React makes it easy to manage local component state with useState and useReducer. But as apps grow, so does the complexity of state. Passing data through multiple layers of components (prop drilling) becomes frustrating. That’s where state management patterns and libraries beyond React come in.
In this blog, we’ll cover when to lift state, when to use global stores, and explore popular tools like Redux Toolkit, React Query, Zustand, MobX, and even a Context + Reducer pattern.
1. When to Move State Up vs External Store
Definition:
Moving state up → Keeping shared state in a common parent component to avoid prop drilling.
External store (global state) → Keeping state outside React components (e.g., Redux, Zustand) so multiple parts of the app can access it directly.
Explanation:
Use lifting state up if only a few components need the data.
Use an external store if many unrelated parts of the app need the same state (e.g., user authentication, theme, shopping cart).
Example (Prop Drilling ❌):
function App() {
const [theme, setTheme] = React.useState("dark");
return <Toolbar theme={theme} />;
}
function Toolbar({ theme }) {
return <ThemedButton theme={theme} />;
}
function ThemedButton({ theme }) {
return <button>Theme is {theme}</button>;
}
Better: Use Context (Global State ✅)
const ThemeContext = React.createContext();
function App() {
const [theme] = React.useState("dark");
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
</ThemeContext.Provider>
);
}
function ThemedButton() {
const theme = React.useContext(ThemeContext);
return <button>Theme is {theme}</button>;
}
2. Redux Toolkit → Slices, createAsyncThunk, Store Configuration
Definition:
Redux Toolkit (RTK) is the official, recommended way to use Redux. It reduces boilerplate and makes managing global state easier.
Key Features:
Slices → Group state + reducers.
createAsyncThunk → Handles async operations like API calls.
Configure Store → Sets up Redux store in one line.
Example:
import { configureStore, createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { Provider, useSelector, useDispatch } from "react-redux";
// Async API call
export const fetchUser = createAsyncThunk("user/fetch", async () => {
const res = await fetch("/api/user");
return res.json();
});
const userSlice = createSlice({
name: "user",
initialState: { data: null, status: "idle" },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => { state.status = "loading"; })
.addCase(fetchUser.fulfilled, (state, action) => {
state.status = "succeeded";
state.data = action.payload;
});
}
});
const store = configureStore({ reducer: { user: userSlice.reducer } });
function App() {
const dispatch = useDispatch();
const user = useSelector((state) => state.user.data);
return (
<div>
<button onClick={() => dispatch(fetchUser())}>Load User</button>
{user && <h2>{user.name}</h2>}
</div>
);
}
export default function Root() {
return <Provider store={store}><App /></Provider>;
}
👉 Redux Toolkit is best when you need structured global state and complex async logic.
3. React Query (TanStack Query) → Server State, Caching, Mutations
Definition:
React Query (now called TanStack Query) is not for UI state but for server state. It handles fetching, caching, and syncing with APIs.
Why it’s useful:
Auto-caches API responses
Refetches stale data
Manages loading and error states
Example:
import { useQuery, useMutation, QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
function UserList() {
const { data, isLoading, error } = useQuery(["users"], () =>
fetch("/api/users").then(res => res.json())
);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error loading users</p>;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<UserList />
</QueryClientProvider>
);
}
👉 Best for remote server state, not UI-only state.
4. Zustand / Jotai / Recoil → Lightweight Alternatives to Redux
Zustand:
Simple store with hooks.
No boilerplate.
Example (Zustand):
import create from "zustand";
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 }))
}));
function Counter() {
const { count, increase } = useStore();
return <button onClick={increase}>Count: {count}</button>;
}
Jotai:
Uses atoms (like Recoil).
Minimal and hooks-friendly.
Recoil:
Provides shared atoms/selectors.
Works well for deeply nested state.
👉 These are good when you want lightweight global state without Redux’s complexity.
5. MobX → Observable State and Computed Values
Definition:
MobX is a reactive state management library where state becomes observable and components react automatically to changes.
Example:
import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";
class CounterStore {
count = 0;
constructor() {
makeAutoObservable(this);
}
increase() {
this.count++;
}
}
const store = new CounterStore();
const Counter = observer(() => (
<button onClick={() => store.increase()}>
Count: {store.count}
</button>
));
👉 MobX shines when you want reactive, computed values and minimal boilerplate.
6. Context + Reducer Pattern → Minimal Global State Without Extra Libraries
Definition:
Instead of Redux, we can combine React Context with useReducer to build a small global store.
Example:
const CounterContext = React.createContext();
function counterReducer(state, action) {
switch (action.type) {
case "increment": return { count: state.count + 1 };
default: return state;
}
}
function CounterProvider({ children }) {
const [state, dispatch] = React.useReducer(counterReducer, { count: 0 });
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}
function Counter() {
const { state, dispatch } = React.useContext(CounterContext);
return (
<button onClick={() => dispatch({ type: "increment" })}>
Count: {state.count}
</button>
);
}
function App() {
return (
<CounterProvider>
<Counter />
</CounterProvider>
);
}
👉 This approach is great for small apps that need a bit of global state without pulling in a big library.
🎯 Final Thoughts
Lift State Up → Use for small apps, avoid unnecessary global stores.
Redux Toolkit → Great for structured, large-scale state management.
React Query → Perfect for API/server state, caching, and syncing.
Zustand / Jotai / Recoil → Lightweight
alternatives, less boilerplate.
MobX → Reactive, observable-based state management.
Context + Reducer → Minimal global state for small apps.
🚀 Choosing the right tool depends on your app’s complexity and scale. For small apps, stick with Context + Reducer or Zustand. For enterprise-level apps, go with Redux Toolkit + React Query.
Comments
Post a Comment