DEV Community

Giorgi Parunov
Giorgi Parunov

Posted on

How I’m Still Managing State in React with Just Context (and Why It Still Works in 2025)

If you've been working with React for a while, you know the state management landscape can get overwhelming fast. Zustand, Jotai, Recoil, Redux Toolkit, atoms, proxies, signals—you name it.

But here’s the thing:

I’m still using React’s built-in Context API with useReducer—and it’s working just fine.
I want to show you how I structure it, why it still holds up in 2025, and what I’ve learned by keeping things simple.

Why I Haven’t Switched to Zustand, Redux, or Jotai

I’ve played around with a lot of state management libraries over the years. They’re impressive in their own ways. Zustand is sleek. Jotai is elegant. Redux (especially with Toolkit) has matured a lot.

But at the end of the day, my projects just haven’t needed the extra overhead.
Context and useReducer give me:

  • Full control and transparency
  • No new dependencies or mental models
  • Easy debugging and testing
  • Just enough flexibility for most app-scale needs

For example, here’s exactly how I manage my shopping bag state in a React app:

import {
  createContext,
  Dispatch,
  FC,
  ReactNode,
  useContext,
  useEffect,
  useMemo,
  useReducer,
} from "react";

interface BagItem {
  id: string;
  name: string;
  quantity: number;
  price: number;
}

interface BagState {
  bag: BagItem[];
  checkoutIsOpen: boolean;
}

interface BagContext {
  state: BagState;
  setState: Dispatch<Partial<BagState>>;
}

const initialState: BagState = {
  bag: [],
  checkoutIsOpen: false,
};

export const BagContext = createContext<Partial<BagContext>>({});

export const useBag: () => Partial<BagContext> = () => useContext(BagContext);

interface BagProviderProps {
  children: ReactNode;
}

const BagProvider: FC<BagProviderProps> = ({ children }) => {
  const [state, setState] = useReducer(
    (oldState: BagState, newState: Partial<BagState>) => ({
      ...oldState,
      ...newState,
    }),
    initialState
  );

  useEffect(() => {
    const localBag = localStorage.getItem("bag");
    if (localBag) {
      const payload: BagItem[] = JSON.parse(localBag);
      setState({ bag: payload });
    }
  }, []);

  const api = useMemo(
    (): BagContext => ({
      state,
      setState,
    }),
    [state]
  );

  return (
    <BagContext.Provider value={api}>
      {children}
    </BagContext.Provider>
  );
};

export default BagProvider;
Enter fullscreen mode Exit fullscreen mode

This setup gives me:

  • A globally accessible bag state
  • A clean useBag() hook
  • Persisted state via localStorage
  • No external libraries

So Why Not Reach for Something Else?

If I ever find myself dealing with complex derived state, undo/redo functionality, or shared state across iframes/tabs—then sure, I might reach for Zustand or Redux.
But most apps don’t need that.
React’s built-in tools have improved, especially with features like:

  • useOptimistic (React 19)
  • Server Actions (Next.js)
  • React Server Components

And as React continues to shift work to the server, I think we’ll rely less on big client-side state tools—not more.

Final Thoughts

You don’t always need to chase the next hot library.
If Context + hooks do the job—and they often do—stick with them.
This setup has worked for me across a bunch of apps, and until I feel real pain, I’m happy keeping things simple and transparent.

What about you? Are you managing state the old-fashioned way, or have you gone all-in on Jotai, Zustand, or Signals?
Let’s chat—drop a comment with your current stack 👇

Top comments (0)