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 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.
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 @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.
Redux is a predictable state container for JavaScript apps.
It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger.
You can use Redux together with React, or with any other view library. The Redux core is tiny (2kB, including dependencies), and has a rich ecosystem of addons.
Redux Toolkit is our official recommended approach for writing Redux logic. It wraps around the Redux core, and contains packages and functions that we think are essential for building a Redux app. Redux Toolkit builds in our suggested best practices, simplifies most Redux tasks, prevents common mistakes, and makes it easier to write Redux applications.
The recommended way to start new apps with React and Redux Toolkit is by using our official Redux Toolkit + TS template for Vite, or by creating a new Next.js project using Next's with-redux template.
Both of these already have Redux Toolkit and React-Redux configured appropriately for that build tool, and come with a small example app that demonstrates how to use several of Redux Toolkit's features.
# Vite with our Redux+TS template
# (using the `degit` tool to clone and extract the template)
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
# Next.js using the `with-redux` template
npx create-next-app --example with-redux my-app
We do not currently have official React Native templates, but recommend these templates for standard React Native and for Expo:
npm install @reduxjs/toolkit react-redux
For the Redux core library by itself:
npm install redux
For more details, see the Installation docs page.
The Redux core docs are located at https://redux.js.org, and include the full Redux tutorials, as well usage guides on general Redux patterns:
The Redux Toolkit docs are available at https://redux-toolkit.js.org, including API references and usage guides for all of the APIs included in Redux Toolkit.
The Redux Essentials tutorial is a "top-down" tutorial that teaches "how to use Redux the right way", using our latest recommended APIs and best practices. We recommend starting there.
The Redux Fundamentals tutorial is a "bottom-up" tutorial that teaches "how Redux works" from first principles and without any abstractions, and why standard Redux usage patterns exist.
The #redux channel of the Reactiflux Discord community is our official resource for all questions related to learning and using Redux. Reactiflux is a great place to hang out, ask questions, and learn - please come and join us there!
Redux is a valuable tool for organizing your state, but you should also consider whether it's appropriate for your situation. Please don't use Redux just because someone said you should - instead, please take some time to understand the potential benefits and tradeoffs of using it.
Here are some suggestions on when it makes sense to use Redux:
Yes, these guidelines are subjective and vague, but this is for a good reason. The point at which you should integrate Redux into your application is different for every user and different for every application.
For more thoughts on how Redux is meant to be used, please see:
The whole global state of your app is stored in an object tree inside a single store. The only way to change the state tree is to create an action, an object describing what happened, and dispatch it to the store. To specify how state gets updated in response to an action, you write pure reducer functions that calculate a new state based on the old state and the action.
Redux Toolkit simplifies the process of writing Redux logic and setting up the store. With Redux Toolkit, the basic app logic looks like:
import { createSlice, configureStore } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
incremented: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decremented: state => {
state.value -= 1
}
}
})
export const { incremented, decremented } = counterSlice.actions
const store = configureStore({
reducer: counterSlice.reducer
})
// Can still subscribe to the store
store.subscribe(() => console.log(store.getState()))
// Still pass action objects to `dispatch`, but they're created for us
store.dispatch(incremented())
// {value: 1}
store.dispatch(incremented())
// {value: 2}
store.dispatch(decremented())
// {value: 1}
Redux Toolkit allows us to write shorter logic that's easier to read, while still following the original core Redux behavior and data flow.
You can find the official logo on GitHub.
This project adheres to Semantic Versioning. Every release, along with the migration instructions, is documented on the GitHub Releases page.