
State management in React has evolved significantly over the years. With new libraries and patterns emerging, it's essential to understand the landscape and choose the right solution for your project. Having worked with various state management approaches at Dentira and Dentira Labs, I've learned that there's no one-size-fits-all solution—context matters.
The React ecosystem now offers more choices than ever before. While Redux remains popular, newer solutions like Zustand, Jotai, and React Query have gained significant traction for specific use cases.
Different state management solutions solve different problems:
Zustand has become my go-to choice for many projects. It's minimal, performant, and incredibly easy to use.
import create from 'zustand';
interface StoreState {
count: number;
user: User | null;
increment: () => void;
setUser: (user: User) => void;
}
const useStore = create<StoreState>((set) => ({
count: 0,
user: null,
increment: () => set((state) => ({ count: state.count + 1 })),
setUser: (user) => set({ user }),
}));
Advantages:
When to use:
At Dentira Labs, we used Zustand for managing scanner configurations:
interface ScannerStore {
activeScanner: ScannerType | null;
scannerConfig: ScannerConfig;
setActiveScanner: (scanner: ScannerType) => void;
updateConfig: (config: Partial<ScannerConfig>) => void;
}
export const useScannerStore = create<ScannerStore>((set) => ({
activeScanner: null,
scannerConfig: defaultConfig,
setActiveScanner: (scanner) => set({ activeScanner: scanner }),
updateConfig: (config) =>
set((state) => ({
scannerConfig: { ...state.scannerConfig, ...config },
})),
}));
Redux Toolkit (RTK) has modernized Redux, making it more approachable while maintaining its power.
import { createSlice, configureStore } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload);
},
},
});
export const store = configureStore({
reducer: {
cart: cartSlice.reducer,
},
});
Advantages:
When to use:
React Query revolutionized how we handle server state. It's not a replacement for client state management but complements it perfectly.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
function useAddProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createProduct,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
Advantages:
When to use:
React's Context API remains useful for certain scenarios, though it has limitations.
const ThemeContext = createContext<ThemeContextType>({
theme: 'light',
toggleTheme: () => {},
});
export function ThemeProvider({ children }: Props) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const value = useMemo(
() => ({
theme,
toggleTheme: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')),
}),
[theme]
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Advantages:
Limitations:
When to use:
Jotai takes an atomic approach to state management, breaking state into small, independent atoms.
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubled] = useAtom(doubledAtom);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
</div>
);
}
Advantages:
When to use:
Application Size:
State Type:
Team Experience:
Performance Requirements:
DevTools Need:
Don't use one solution for everything:
// ✅ Good: Separate server and client state
const { data: products } = useQuery(['products'], fetchProducts); // Server state
const cart = useCartStore(); // Client state
// ❌ Bad: Mixing everything in one store
const { products, cart, user } = useEverythingStore();
Begin with the simplest solution that works:
Strong typing prevents bugs and improves DX:
// ✅ Typed store
interface AppStore {
user: User | null;
setUser: (user: User) => void;
}
// ❌ Untyped store
const useStore = create((set) => ({ ... }));
Use selectors to prevent unnecessary re-renders:
// ✅ Only re-renders when user.name changes
const userName = useStore((state) => state.user?.name);
// ❌ Re-renders on any store change
const { user } = useStore();
Don't lift state too high. Keep state as local as possible:
// ✅ Local state for component-specific data
const [isOpen, setIsOpen] = useState(false);
// ✅ Global state for shared data
const user = useUserStore();
If you're working with legacy Redux code, consider these patterns:
Looking ahead, I see these trends:
The state management landscape in 2025 offers excellent choices for every scenario. The key is understanding your requirements and choosing accordingly. Don't overcomplicate—start simple and scale up when needed.
At Dentira Labs, we've successfully used a combination of Zustand for client state and React Query for server state. This combination provides excellent developer experience while keeping our bundle size reasonable.
Remember: the best state management solution is the one that your team can use effectively and that solves your specific problems. Experiment, learn, and don't be afraid to try new approaches as they emerge.