redux, mobx, zustand, and @hookstate/core are all solutions for managing shared state in React applications, but they approach the problem from different angles. redux relies on a single immutable store updated via pure functions, offering strict predictability at the cost of boilerplate. mobx uses observables to track dependencies automatically, allowing mutable state that reacts to changes. zustand provides a lightweight hook-based store with minimal setup, blending simplicity with flexibility. @hookstate/core leverages React hooks and proxies to manage state directly within components, aiming for high performance with less code.
When building React applications, managing data that changes over time is one of the most critical architectural decisions. redux, mobx, zustand, and @hookstate/core all solve this problem, but they work differently under the hood. Let's compare how they handle setup, updates, and rendering in real-world scenarios.
redux requires a centralized store configuration, usually enhanced by Redux Toolkit.
// redux: Configure store with slices
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: { increment: (state) => { state.value += 1; } }
});
const store = configureStore({ reducer: { counter: counterSlice.reducer } });
mobx uses observable objects that can be created anywhere.
// mobx: Create observable store
import { makeAutoObservable } from 'mobx';
class CounterStore {
value = 0;
constructor() { makeAutoObservable(this); }
increment() { this.value += 1; }
}
const store = new CounterStore();
zustand creates a store using a simple function that returns state and actions.
// zustand: Create store hook
import { create } from 'zustand';
const useCounterStore = create((set) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 }))
}));
@hookstate/core does not require a separate store definition.
// @hookstate/core: Initialize state directly
import { useHookstate } from '@hookstate/core';
function Component() {
const state = useHookstate({ value: 0 });
// State lives within the component tree
}
redux relies on dispatching actions to trigger pure reducer functions.
// redux: Dispatch an action
import { useDispatch } from 'react-redux';
function Component() {
const dispatch = useDispatch();
const handleIncrement = () => dispatch({ type: 'counter/increment' });
return <button onClick={handleIncrement}>Add</button>;
}
mobx allows direct mutation of observable properties.
// mobx: Direct mutation
import { observer } from 'mobx-react-lite';
const Component = observer(() => {
const handleIncrement = () => { store.value += 1; };
return <button onClick={handleIncrement}>Add</button>;
});
zustand uses a set function to update state immutably.
set within action methods defined in the store.setState but for global data.// zustand: Call set function
function Component() {
const increment = useCounterStore((s) => s.increment);
return <button onClick={increment}>Add</button>;
}
@hookstate/core lets you mutate state directly via proxies.
// @hookstate/core: Direct assignment
function Component() {
const state = useHookstate({ value: 0 });
const handleIncrement = () => { state.value.set(state.value.get() + 1); };
return <button onClick={handleIncrement}>Add</button>;
}
redux uses selectors to extract data from the store.
useSelector ensures the component only updates when relevant data changes.// redux: Select state slice
function Component() {
const value = useSelector((state) => state.counter.value);
return <div>{value}</div>;
}
mobx wraps components in an observer to track usage.
// mobx: Observer wrapper
const Component = observer(() => {
return <div>{store.value}</div>;
});
zustand uses selectors within the hook to prevent unnecessary renders.
// zustand: Select specific field
function Component() {
const value = useCounterStore((state) => state.value);
return <div>{value}</div>;
}
@hookstate/core uses .get() to read values within components.
// @hookstate/core: Get value
function Component() {
const state = useHookstate({ value: 0 });
return <div>{state.value.get()}</div>;
}
redux handles async logic with thunks or sagas.
createAsyncThunk for standard cases.// redux: Async thunk
const fetchUser = createAsyncThunk('user/fetch', async (id) => {
const res = await fetch(`/api/${id}`);
return res.json();
});
mobx uses flow to handle generators or async actions.
// mobx: Flow for async
import { flow } from 'mobx';
class Store {
fetchData = flow(function* (id) {
const res = yield fetch(`/api/${id}`);
this.data = yield res.json();
});
}
zustand handles async logic directly inside the set function.
// zustand: Async in action
const useStore = create((set) => ({
fetchData: async (id) => {
const res = await fetch(`/api/${id}`);
set({ data: await res.json() });
}
}));
@hookstate/core uses standard async/await patterns.
// @hookstate/core: Standard async
async function loadData(state) {
const res = await fetch('/api/data');
state.data.set(await res.json());
}
The amount of code you write varies significantly between these tools.
redux has the most structure. You define slices, actions, and store configuration. This adds files but enforces consistency.mobx reduces boilerplate by removing actions and reducers. You write classes or objects with methods.zustand is minimal. One file often holds the store definition and actions together.@hookstate/core removes store files entirely. State lives where it is used.| Feature | redux | mobx | zustand | @hookstate/core |
|---|---|---|---|---|
| Setup | 🏗️ Centralized Store | 🧩 Observable Objects | 🪝 Hook Store | 🪝 Local Hooks |
| Updates | 📝 Dispatch Actions | ✏️ Direct Mutation | 🔄 Set Function | ✏️ Direct Mutation |
| Reading | 🔍 Selectors | 👁️ Observer Wrapper | 🔍 Selectors | 🔍 .get() Method |
| Async | 📦 Thunks/Sagas | 🌊 Flow/Actions | ⚡ Native Async | ⚡ Native Async |
| Boilerplate | 📄 High (Reduced with RTK) | 📄 Medium | 📄 Low | 📄 Very Low |
redux is like a strict blueprint 📐 — best for large teams needing consistency, debugging tools, and a clear history of changes. It shines in enterprise apps where predictability matters more than speed of setup.
mobx is like a reactive spreadsheet 📊 — ideal for complex domains with many interdependent values. It reduces code but requires understanding how observables track dependencies.
zustand is like a lightweight toolkit 🎒 — perfect for most modern React apps that need shared state without the fuss. It balances simplicity and power effectively.
@hookstate/core is like a direct wire 🔌 — great for developers who want to manage state exactly where it is used without indirection. It offers high performance with minimal abstraction.
Final Thought: All four libraries can build robust applications. Your choice should depend on how much structure your team needs versus how quickly you want to ship features. For new projects today, zustand or redux (with Toolkit) are the most common choices, while mobx and @hookstate/core serve specific architectural preferences well.
Choose @hookstate/core if you want a hook-based solution that removes the need for separate store files and reducers. It is ideal for projects where you prefer managing state directly within components using proxies, without the setup overhead of larger libraries. This works well for medium-sized apps that need better performance than standard Context API but less structure than Redux.
Choose mobx if you prefer an object-oriented approach where state mutations feel like normal JavaScript assignments. It is suitable for complex applications with deep dependency graphs, as it automatically tracks which components need to re-render. This is a strong fit for teams comfortable with observables and who want to avoid manual selector optimization.
Choose redux if you need a strict, predictable state container with a powerful ecosystem of devtools and middleware. It is best for large-scale applications where time-travel debugging, strict action logging, and centralized state logic are critical. Modern Redux Toolkit reduces the boilerplate significantly, making it viable for new projects that require robust architecture.
Choose zustand if you want a minimal, hook-based store that avoids providers and boilerplate. It is perfect for projects that need shared state without the complexity of Redux or the observables of MobX. This library shines when you want quick setup, easy TypeScript integration, and the ability to split stores without performance loss.
The most straightforward, extensible and incredibly fast state management that is based on React state hook.
Why? • Docs / Samples • Demo application • Extensions • Release notes
Any questions? Just ask by raising a github ticket.