State Management for React
Redux, Zustand
When to use Global state manager?
- Multiple Components Need the Same State
- For example, user authentication info (userId, token, etc.), theme settings, or language preferences.
- You don’t want to keep drilling props down through multiple layers.
- State Needs to Persist Across Routes
- For example, a shopping cart that’s accessible from multiple pages, or filters on a dashboard.
Avoid Global State When:
- The state is local to a component or small part of the tree
- Local
useState
oruseReducer
is usually enough and better for encapsulation and simplicity.
- You only need props drilling one or two levels
- Avoid overengineering. Sometimes passing props is totally fine.
- You’re just doing UI state (e.g., modals, tabs)
- Local state or something like a UI store (if really needed) is better.
Redux
Redux is a state management library that allows accessing global state. This means that state can be accessed by any component, regardless of where it is in the tree.
Key Concepts:
- Centralized store
- Immutable state
- Pure reducer functions
- Actions and dispatching
- Middleware (like thunk/saga) for async logic
Example use cases:
- Enterprise applications
- Complex data flows (e.g., financial dashboards, apps with tons of forms and user roles)
- Apps with optimistic updates or time-travel debugging
Redux toolkit will allow you to easily set redux up and connect it to your react application.
Because we are using createSlice from Redux toolkit this will do things behind the scenes without us having to set up anthing.

3 main concepts of Redux: Store, Actions and Reducers
Store:
A store is just a state that may be defined in any way and contain any content. Stores are typically made up of many slices, each with different responsibilities for a specific domain in the application.
Action:
Actions represent what you want Redux to perform with the state. The payload is optional and can be whatever data that you wish to deliver to Redux in your action so that it can do its function. You can think of an action as an event that describes something that happened in the application.
Reducer:
Reducers are responsible for taking an action and then depending on the type of the action will actually and make the update in the Redux store. Use the type of the action to know what updates to do and optionally they will use the payload to do those specific updates to the Redux store.
Reducer will never directly make an update to the Redux store. It will immutable data. It means that it will never allowe to directly mutate the state Reducer will take the state and it will make a copy of the state, then it will make changes to that copy of the state and after that will completely replace the state as a whole with the copy that has the changes applied. You can think of a reducer as an event listener which handles events based on the received action (event) type.
Dispatch:
The Redux store has a method called dispatch. The only way to update the state is to call store.dispatch() and pass in an action object.
Selectors:
Selectors are functions that know how to extract specific pieces of information from a store state value. As an application grows bigger, this can help avoid repeating logic as different parts of the app need to read the same data
Redux toolkit
Example: Vite + React
Example: Nextjs
Zustand
Key Concepts:
- Uses hooks to access state
- No context/provider needed
- Faster then context - it allows you to specificly select what state you want to subscribe to
- State merging by default
- Extendable by default - you can add custom middleware to give more functionality
- Works well with React’s modern features
- Directly integrates into components
Diferent between context and zustand
If you have context with valueA and valueB and you want to access valueA in a component, you will be subscribed to all states in that context. This is a problem, because if you change valueB, your component will also re-render.
With zustand store, we can have multiple values in the store, but another component will subscribe only to the value that wants to subscribe. which means that it will re-render only if valueA changes.
Are re-renders bad?
No, if you have complex react logic, witch shoud be in useMemo or useEffect. If value doesnt change that often or is not that complext then it not problem for react to handle.
Renders dont change the DOM - all computation are on shadow DOM and only if there are diference between shadow DOM and actually DOM, there will be commands to change the DOM. With is very expensive part
When to use Zustand
- You’re building a small to medium app, or starting a new project and want something quick and modern.
- You like React hooks and don’t want to deal with context or reducers.
- You want state logic co-located with components, not in global files.
- You care about performance and simplicity (Zustand updates are fine-grained and efficient).
- You want a “set it and forget it” store with zero boilerplate.
Example use cases:
- SPAs and mobile apps (React Native)
- Games or animation-heavy apps
- MVPs, prototypes, or internal dashboards
- Component libraries or micro-frontends
Example: Vite + React
Example: Nextjs
More:
https://youtu.be/fZPgBnL2x-Q?si=3lgPRdaJEHemPnCH&t=802
MobX
The main responsibility of stores is to move logic and state out of your components into a standalone testable unit that can be used in both frontend and backend JavaScript.
Key Concepts:
- Observable state
- Reactions and computed values
- state is muttable
- OOP-friendly (classes)
A single domain store should be responsible for a single concept in your application. A single store is often organized as a tree structure with multiple domain objects inside.
For example: one domain store for your products, and one for your orders and orderlines
- observable - Defines a trackable field that stores the state.
- computed - Computed values can be used to derive information from other observables
- reaction - Automatically running side effects whenever something relevant changes
- action - An action is any piece of code that modifies the state
observer
Observer is HOC that you can wrap around a React component. It will automatically subscribe to any observables that are used during rendering. As a result, components will automatically re-render when relevant observables change. It also makes sure that components don't re-render when there are no relevant changes For observer to work, it doesn't matter how the observables arrive in the component, only that they are read.
observer
automatically applies memo, so observer
components never need to be wrapped in memo.
Always read observables inside observer components
If observer
is used in server side rendering context; make sure to call enableStaticRendering(true), so that observer won't subscribe to any observables used, and no GC problems are introduced
When to use MobX
- You prefer an OOP/reactive programming style and want to think less about immutability.
- You want a lightweight, magic-like reactivity system that just "works" with minimal boilerplate.
- Your app's state is deeply nested or complex, and you don't want to write reducers.
- You’re building a medium to large app, but want less ceremony than Redux.
- You or your team are comfortable with the idea of observable state and automatic reactions.
Example use cases:
- Apps with heavy UI interactions (like forms, editors)
- Real-time collaboration tools
- Internal tools where speed of development matters more than formal structure
Using observables inside React components adds value as soon as they are either 1) deep, 2) have computed values or 3) are shared with other observer components.
It doesn't matter how we got the reference to an observable, we can consume observables from outer scopes directly (including from imports, etc.)
Using observables directly works very well, but since this typically introduces module state, this pattern might complicate unit testing. Instead, we recommend using React Context instead.
Example: Vite + React
the TimerView component would not react to future changes if it was defined as follows, because the .secondsPassed is not read inside the observer component, but outside, and is hence not tracked.