redux vs zustand vs mobx vs @hookstate/core
State Management Strategies in React Applications
reduxzustandmobx@hookstate/coreSimilar Packages:

State Management Strategies in React Applications

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.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
redux32,516,27361,453290 kB422 years agoMIT
zustand32,169,82257,85295 kB8a month agoMIT
mobx3,471,67828,1834.35 MB807 months agoMIT
@hookstate/core109,8061,657498 kB32a year agoMIT

State Management in React: Redux vs MobX vs Zustand vs Hookstate

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.

πŸ—οΈ Setting Up the Store: Centralized vs Distributed

redux requires a centralized store configuration, usually enhanced by Redux Toolkit.

  • You define slices of state and combine them into a single root store.
  • This enforces a single source of truth but requires initial setup.
// 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.

  • You don't need a single root store; you can have multiple store instances.
  • State is marked as observable to enable reactive tracking.
// 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.

  • No providers are needed; you import the hook directly into components.
  • Multiple stores are encouraged for modularity.
// 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.

  • You initialize state directly inside components or custom hooks.
  • It uses proxies to track usage without explicit observables.
// @hookstate/core: Initialize state directly
import { useHookstate } from '@hookstate/core';

function Component() {
  const state = useHookstate({ value: 0 });
  // State lives within the component tree
}

✍️ Updating State: Mutations vs Actions

redux relies on dispatching actions to trigger pure reducer functions.

  • You cannot mutate state directly; you return a new copy.
  • Redux Toolkit allows immutable syntax using Immer under the hood.
// 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.

  • Changes are tracked automatically, triggering reactions.
  • No need to dispatch actions or write reducers.
// 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.

  • You call set within action methods defined in the store.
  • It feels similar to React's 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.

  • Assigning a value triggers a re-render in dependent components.
  • No dispatchers or setters are required.
// @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>;
}

πŸ‘οΈ Reading Data: Selectors vs Observers

redux uses selectors to extract data from the store.

  • Components subscribe to specific slices of state.
  • 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.

  • Any observable read inside the component triggers a subscription.
  • You access properties directly without selectors.
// mobx: Observer wrapper
const Component = observer(() => {
  return <div>{store.value}</div>;
});

zustand uses selectors within the hook to prevent unnecessary renders.

  • You pass a function to the hook to pick specific fields.
  • This avoids re-rendering when unrelated state changes.
// 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.

  • The hook tracks which properties are accessed.
  • Only components accessing changed data will re-render.
// @hookstate/core: Get value
function Component() {
  const state = useHookstate({ value: 0 });
  return <div>{state.value.get()}</div>;
}

⚑ Async Logic: Thunks vs Flows vs Native

redux handles async logic with thunks or sagas.

  • Redux Toolkit provides createAsyncThunk for standard cases.
  • You manage loading and error states in the reducer.
// 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.

  • Async functions are wrapped to track pending states.
  • Mutations happen directly after await resolves.
// 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.

  • You can call async functions within actions without extra tools.
  • Simple and straightforward for most use cases.
// 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.

  • You update state directly after the promise resolves.
  • No special wrappers are needed for asynchronous code.
// @hookstate/core: Standard async
async function loadData(state) {
  const res = await fetch('/api/data');
  state.data.set(await res.json());
}

🧩 Boilerplate & Structure

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.

πŸ“Š Summary Table

Featurereduxmobxzustand@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

πŸ’‘ The Big Picture

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.

How to Choose: redux vs zustand vs mobx vs @hookstate/core

  • redux:

    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.

  • zustand:

    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.

  • mobx:

    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.

  • @hookstate/core:

    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.

README for redux

Redux Logo

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.

GitHub Workflow Status npm version npm downloads redux channel on discord

Installation

Create a React Redux App

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.

Documentation

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.

Learn Redux

Redux Essentials Tutorial

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.

Redux Fundamentals Tutorial

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.

Help and Discussion

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!

Before Proceeding Further

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:

  • You have reasonable amounts of data changing over time
  • You need a single source of truth for your state
  • You find that keeping all your state in a top-level component is no longer sufficient

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:

Basic Example

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.

Logo

You can find the official logo on GitHub.

Change Log

This project adheres to Semantic Versioning. Every release, along with the migration instructions, is documented on the GitHub Releases page.

License

MIT