Back in 2018, if you weren't using Redux, were you even a real React dev?
But these days, the tides have changed. Redux is still around — but it's heavy, verbose, and often overkill for most modern apps.
🚨 Redux isn't bad — it's just not worth the overhead for 90% of use cases.
I've replaced Redux in every new project. What do I use instead? Here's the modern state stack I reach for:
☕ The Modern State Trio
- Zustand — for global state, stores, and shared logic
- TanStack Query — for all things async (fetching, caching, background syncing)
- Recoil — when state is deeply nested or shared across non-hierarchical components
🐻 Zustand: State That Feels Like React
Zustand is minimal, intuitive, and requires no boilerplate. No providers. No reducers. No headaches.
import { create } from 'zustand'
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
}))
Then inside your component:
const AddToCart = ({ product }) => {
const addItem = useCartStore((state) => state.addItem)
return (
<button onClick={() => addItem(product)}>
Add to cart
</button>
)
}
No setup, no reducer ceremony. Just functional, fast state logic.
⚡ TanStack Query: Async Done Right
Stop putting API responses in your global state. TanStack Query (ex React Query) handles fetching, caching, loading, refetching, and even pagination for you.
import { useQuery } from "@tanstack/react-query";
const useProducts = () => useQuery({
queryKey: ["products"],
queryFn: () => fetch("/api/products").then((res) => res.json()),
});
Use it like this:
const ProductList = () => {
const { data, isLoading } = useProducts();
if (isLoading) return <p>Loading...</p>;
return (
<ul>
{data.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}
This handles caching, background refetching, retrying, and stale data by default. Imagine doing that in Redux 🙃
🧬 Recoil: The Power of Atoms
When you need to share state between unrelated components or manage complex dependency trees, Recoil steps in.
import { atom, useRecoilState } from 'recoil'
const themeAtom = atom({
key: "theme",
default: "light"
})
Use it anywhere:
const ThemeToggle = () => {
const [theme, setTheme] = useRecoilState(themeAtom)
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle to {theme === "light" ? "dark" : "light"}
</button>
)
}
Atoms feel like individual pieces of state — not centralized stores. They're great when your app starts to outgrow local state but doesn't need Redux-level architecture.
🪦 RIP Redux (For Most Use Cases)
Redux will always have a place in the React history books. But for today's DX-focused, server-first, API-heavy apps, it's just not the tool I reach for anymore.
If you're bootstrapping a new project in 2024, here's my rule of thumb:
- Global state? Zustand
- Async/server state? TanStack Query
- State shared across layout trees? Recoil
And if none of those fit? Reach for React's built-in useContext + useReducer
combo — it still works great for lightweight needs.
💡 Final Thought
Redux isn't dead. But for most modern apps, it's in early retirement — living in middleware land with its reducers and action types.
Modern state tools are faster to write, easier to scale, and less mentally taxing. Don't fight your tools. Use the ones that get out of your way.