mobx-react vs react-redux vs redux-saga
State Management and Side Effect Handling in React Applications
mobx-reactreact-reduxredux-sagaSimilar Packages:

State Management and Side Effect Handling in React Applications

mobx-react, react-redux, and redux-saga are complementary tools in the React ecosystem for managing application state and handling side effects, but they serve different layers of the architecture. mobx-react provides bindings between React components and MobX observables, enabling automatic reactivity based on data usage. react-redux is the official React binding for Redux, connecting components to a centralized immutable store using explicit subscriptions. redux-saga is a middleware for Redux that manages complex asynchronous workflows and side effects using generator functions, and it is not a standalone state management solution but rather an extension to Redux.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
mobx-react028,213402 kB46a month agoMIT
react-redux023,425828 kB404 months agoMIT
redux-saga0-36.8 kB-2 months agoMIT

State Management and Side Effects: mobx-react vs react-redux vs redux-saga

These three packages often appear together in discussions about React architecture, but they solve different problems and operate at different layers. Let’s clarify their roles and compare how they work in real applications.

🧩 Core Responsibilities: What Each Package Actually Does

mobx-react connects React components to MobX observables. When a component uses observable data (via observer), it automatically re-renders when that data changes—no manual subscriptions needed.

// mobx-react: Automatic reactivity
import { observer } from 'mobx-react';
import userStore from './stores/userStore';

const UserProfile = observer(() => {
  return <div>{userStore.name}</div>; // Re-renders when name changes
});

react-redux connects React components to a Redux store. Components must explicitly declare which parts of state they need using useSelector, and updates happen only when those selected values change.

// react-redux: Explicit subscription
import { useSelector } from 'react-redux';

const UserProfile = () => {
  const name = useSelector(state => state.user.name);
  return <div>{name}</div>;
};

redux-saga is not a React binding—it’s a Redux middleware for managing side effects (like API calls) using generator functions. It doesn’t interact with React directly; it listens to dispatched actions and runs async logic.

// redux-saga: Side effect handling (runs in middleware, not in component)
import { call, put, takeEvery } from 'redux-saga/effects';
import { fetchUserSuccess, fetchUserFailure } from './userActions';

function* fetchUserSaga(action) {
  try {
    const user = yield call(api.fetchUser, action.payload.id);
    yield put(fetchUserSuccess(user));
  } catch (error) {
    yield put(fetchUserFailure(error));
  }
}

function* userSaga() {
  yield takeEvery('FETCH_USER_REQUEST', fetchUserSaga);
}

💡 Key Insight: redux-saga cannot replace react-redux. You still need react-redux to connect your components to the Redux store that redux-saga updates.

🔁 Data Flow: Mutable Reactivity vs Immutable Updates

mobx-react embraces a mutable-like model. You modify observable state directly, and MobX tracks which components depend on that state.

// mobx-react: Direct mutation
userStore.setName('Alice'); // Triggers re-render of UserProfile

Under the hood, MobX uses proxies or getters/setters to make this safe, but the syntax feels natural and imperative.

react-redux enforces immutable updates. You never mutate state directly—you dispatch actions that describe changes, and reducers produce new state objects.

// react-redux: Dispatch action
import { useDispatch } from 'react-redux';

const UserProfileEditor = () => {
  const dispatch = useDispatch();
  const updateName = () => dispatch({ type: 'SET_NAME', payload: 'Alice' });
  // Reducer creates new state; useSelector detects change
};

This makes state changes explicit and easier to trace, at the cost of more code.

redux-saga doesn’t change this flow—it just adds a layer for handling async logic before dispatching actions.

// redux-saga enables complex async before dispatch
// e.g., cancel ongoing requests, debounce, or retry logic

📦 Integration Complexity: Boilerplate vs Simplicity

mobx-react requires minimal setup. Define observables, wrap components with observer, and mutate state directly.

// Minimal MobX setup
import { makeAutoObservable } from 'mobx';

class UserStore {
  name = '';
  constructor() {
    makeAutoObservable(this);
  }
  setName(name) {
    this.name = name;
  }
}

react-redux traditionally required more boilerplate (actions, action creators, reducers, mapStateToProps), but Redux Toolkit has dramatically simplified this.

// Modern Redux with Redux Toolkit
import { createSlice } from '@reduxjs/toolkit';

const userSlice = createSlice({
  name: 'user',
  initialState: { name: '' },
  reducers: {
    setName: (state, action) => {
      state.name = action.payload; // Mutative syntax allowed by Immer
    }
  }
});

Even with Toolkit, you still need to set up a store and wrap your app with <Provider>.

redux-saga adds significant complexity: you must write generator functions, understand effects like call/put/takeEvery, and register sagas with middleware.

// Saga setup
import createSagaMiddleware from 'redux-saga';
import { createStore, applyMiddleware } from 'redux';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);

This is only justified for apps with advanced async requirements.

🛠️ Debugging and Tooling

mobx-react offers the MobX DevTools extension, which shows which observables triggered a re-render. However, because reactivity is implicit, it can be harder to trace why a component updated.

react-redux integrates deeply with the Redux DevTools Extension, providing time-travel debugging, action history, and state diffs. Every state change is logged as a dispatched action, making it easy to reproduce bugs.

redux-saga logs saga effects in Redux DevTools, but debugging generator-based logic can be challenging—especially when dealing with cancellation or race conditions.

🔄 Real-World Usage Scenarios

Scenario 1: Simple CRUD App with Local State

  • ✅ Best choice: mobx-react
  • Why? Low boilerplate, direct mutations, and automatic updates match the simplicity of the app.
// No need for actions, reducers, or middleware
const TodoList = observer(() => {
  return (
    <ul>
      {todoStore.todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
    </ul>
  );
});

Scenario 2: Medium-Sized App with Predictable State

  • ✅ Best choice: react-redux + Redux Toolkit
  • Why? Clear data flow, excellent dev tools, and TypeScript support help maintain correctness as the app grows.
// Explicit state shape and updates
const todos = useSelector(state => state.todos.items);

Scenario 3: Complex Workflow with Cancellation (e.g., Live Search)

  • ✅ Best choice: react-redux + redux-saga
  • Why? Sagas can cancel previous search requests when a new one starts.
// redux-saga: Cancel previous search
import { takeLatest } from 'redux-saga/effects';

function* searchSaga(action) {
  const results = yield call(api.search, action.payload.query);
  yield put(searchSuccess(results));
}

function* rootSaga() {
  yield takeLatest('SEARCH_REQUEST', searchSaga); // Auto-cancels
}

⚠️ Common Misconceptions

  • Myth: "redux-saga replaces react-redux."
    Truth: redux-saga only handles side effects. You still need react-redux to connect components to the Redux store.

  • Myth: "MobX is harder to test than Redux."
    Truth: Both are testable. MobX stores can be unit-tested like any class; Redux reducers are pure functions.

  • Myth: "You must use redux-saga for all async logic in Redux."
    Truth: For simple cases (e.g., one API call per action), Redux Thunk (built into Redux Toolkit) is simpler and sufficient.

📊 Summary Table

Aspectmobx-reactreact-reduxredux-saga
Primary RoleReact ↔ MobX bindingReact ↔ Redux bindingRedux middleware for side effects
Data ModelObservable, mutable-likeImmutable, explicit actionsN/A (extends Redux)
BoilerplateLowMedium (reduced by Redux Toolkit)High
Async HandlingBuilt-in (just use async/await)Requires middleware (Thunk/Saga)Specialized for complex async flows
DebuggingGood (DevTools), implicit reactivityExcellent (time-travel, action log)Moderate (generator complexity)
Standalone?Yes (with MobX)Yes (with Redux)❌ (requires Redux)

💡 Final Guidance

  • If you want simplicity and reactivity, go with mobx-react.
  • If you value predictability, tooling, and ecosystem maturity, choose react-redux (preferably with Redux Toolkit).
  • Only add redux-saga if you’re already using Redux and hit the limits of simpler async patterns—don’t reach for it prematurely.

Remember: these tools aren’t mutually exclusive in theory, but mixing MobX and Redux in the same app usually adds unnecessary complexity. Pick one state management philosophy and stick with it.

How to Choose: mobx-react vs react-redux vs redux-saga

  • mobx-react:

    Choose mobx-react if you prefer a reactive programming model where components automatically re-render when the data they use changes, without needing to manually subscribe or select slices of state. It works best when your team values concise, mutable-like syntax with minimal boilerplate and can manage potential debugging complexity from implicit reactivity.

  • react-redux:

    Choose react-redux if you're using Redux and need efficient, predictable connections between your React components and a centralized immutable store. It’s ideal for applications requiring strict unidirectional data flow, time-travel debugging, or strong typing with TypeScript, especially when paired with modern Redux Toolkit patterns.

  • redux-saga:

    Choose redux-saga only if you’re already using Redux and need to handle complex asynchronous logic—like long-running tasks, cancellations, or race conditions—that goes beyond what simpler middleware like Redux Thunk can manage. It should not be used alone; it requires Redux and react-redux to integrate with React components.

README for mobx-react

mobx-react

CircleCI CDNJS Minzipped size Discuss on Github View changelog

Package with React component wrapper for combining React with MobX. Exports the observer decorator and other utilities. For documentation, see the MobX project. This package supports both React and React Native.

Compatibility matrix

Only the latest version is actively maintained. If you're missing a fix or a feature in older version, consider upgrading or using patch-package

NPM VersionSupport MobX versionSupported React versionsAdded support for:
v107.*>=18MobX 7, Hooks, React 18 strict mode
v96.*>16.8Hooks, React 18.2 in strict mode
v76.*>16.8 < 18.2Hooks
v64.* / 5.*>16.8 <17Hooks
v54.* / 5.*>0.13 <17No, but it is possible to use <Observer> sections inside hook based components

mobx-react is a wrapper around mobx-react-lite for applications that also need class component support:

  • Support for function components through mobx-react-lite
  • Support for class based components for observer and @observer

Installation

npm install mobx-react

Or CDN: https://unpkg.com/mobx-react (UMD namespace: mobxReact)

import { observer } from "mobx-react"

This package provides the bindings for MobX and React. See the official documentation for how to get started.

Use React.createContext to pass stores around.

API documentation

Please check mobx.js.org for the general documentation. The documentation below highlights some specifics.

observer(component)

Function (and decorator) that converts a React component definition, React component class, or stand-alone render function, into a reactive component. A converted component will track which observables are used by its effective render and automatically re-render the component when one of these values changes.

Functional Components

React.memo is automatically applied to functional components provided to observer. observer does not accept a functional component already wrapped in React.memo, or an observer, in order to avoid consequences that might arise as a result of wrapping it twice.

Class Components

shouldComponentUpdate is not supported. As such, it is recommended that class components extend React.PureComponent. The observer will automatically patch non-pure class components with an internal implementation of React.PureComponent if necessary.

Extending observer class components is not supported. Always apply observer only on the last class in the inheritance chain.

See the MobX documentation for more details.

import { observer } from "mobx-react"

// ---- ES6 syntax ----
const TodoView = observer(
    class TodoView extends React.Component {
        render() {
            return <div>{this.props.todo.title}</div>
        }
    }
)

// ---- ESNext syntax with decorator syntax enabled ----
@observer
class TodoView extends React.Component {
    render() {
        return <div>{this.props.todo.title}</div>
    }
}

// ---- or just use function components: ----
const TodoView = observer(({ todo }) => <div>{todo.title}</div>)
Note on using props and state in derivations

mobx-react version 6 and lower would automatically turn this.state and this.props into observables. This has the benefit that computed properties and reactions were able to observe those. However, since this pattern is fundamentally incompatible with StrictMode in React 18.2 and higher, this behavior has been removed in React 18.

As a result, we recommend to no longer mark properties as @computed in observer components if they depend on this.state or this.props.

@observer
class Doubler extends React.Component<{ counter: number }> {
    @computed // BROKEN! <-- @computed should be removed in mobx-react > 7
    get doubleValue() {
        // Changes to this.props will no longer be detected properly, to fix it,
        // remove the @computed annotation.
        return this.props * 2
    }

    render() {
        return <div>{this.doubleValue}</div>
    }
}

Similarly, reactions will no longer respond to this.state / this.props. This can be overcome by creating an observable copy:

@observer
class Alerter extends React.Component<{ counter: number }> {
    @observable observableCounter: number
    reactionDisposer

    constructor(props) {
        this.observableCounter = counter
    }

    componentDidMount() {
        // set up a reaction, by observing the observable,
        // rather than the prop which is non-reactive:
        this.reactionDisposer = autorun(() => {
            if (this.observableCounter > 10) {
                alert("Reached 10!")
            }
        })
    }

    componentDidUpdate() {
        // sync the observable from props
        this.observableCounter = this.props.counter
    }

    componentWillUnmount() {
        this.reactionDisposer()
    }

    render() {
        return <div>{this.props.counter}</div>
    }
}

MobX-react will try to detect cases where this.props, this.state or this.context are used by any other derivation than the render method of the owning component and throw. This is to make sure that neither computed properties, nor reactions, nor other components accidentally rely on those fields to be reactive.

This includes cases where a render callback is passed to a child, that will read from the props or state of a parent component. As a result, passing a function that might later read a property of a parent in a reactive context will throw as well. Instead, when using a callback function that is being passed to an observer based child, the capture should be captured locally first:

@observer
class ChildWrapper extends React.Component<{ counter: number }> {
    render() {
        // Collapsible is an observer component that should respond to this.counter,
        // if it is expanded

        // BAD:
        return <Collapsible onRenderContent={() => <h1>{this.props.counter}</h1>} />

        // GOOD: (causes to pass down a fresh callback whenever counter changes,
        // that doesn't depend on its parents props)
        const counter = this.props.counter
        return <Collapsible onRenderContent={() => <h1>{counter}</h1>} />
    }
}

Observer

Observer is a React component, which applies observer to an anonymous region in your component. It takes as children a single, argumentless function which should return exactly one React component. The rendering in the function will be tracked and automatically re-rendered when needed. This can come in handy when needing to pass render function to external components (for example the React Native listview), or if you dislike the observer decorator / function.

class App extends React.Component {
    render() {
        return (
            <div>
                {this.props.person.name}
                <Observer>{() => <div>{this.props.person.name}</div>}</Observer>
            </div>
        )
    }
}

const person = observable({ name: "John" })

ReactDOM.render(<App person={person} />, document.body)
person.name = "Mike" // will cause the Observer region to re-render

In case you are a fan of render props, you can use that instead of children. Be advised, that you cannot use both approaches at once, children have a precedence. Example

class App extends React.Component {
    render() {
        return (
            <div>
                {this.props.person.name}
                <Observer render={() => <div>{this.props.person.name}</div>} />
            </div>
        )
    }
}

const person = observable({ name: "John" })

ReactDOM.render(<App person={person} />, document.body)
person.name = "Mike" // will cause the Observer region to re-render

useLocalObservable hook

Local observable state can be introduced by using the useLocalObservable hook, that runs once to create an observable store. A quick example would be:

import { useLocalObservable, Observer } from "mobx-react"

const Todo = () => {
    const todo = useLocalObservable(() => ({
        title: "Test",
        done: true,
        toggle() {
            this.done = !this.done
        }
    }))

    return (
        <Observer>
            {() => (
                <h1 onClick={todo.toggle}>
                    {todo.title} {todo.done ? "[DONE]" : "[OPEN]"}
                </h1>
            )}
        </Observer>
    )
}

When using useLocalObservable, all properties of the returned object will be made observable automatically, getters will be turned into computed properties, and methods will be bound to the store and apply mobx transactions automatically. If new class instances are returned from the initializer, they will be kept as is.

It is important to realize that the store is created only once! It is not possible to specify dependencies to force re-creation, nor should you directly be referring to props for the initializer function, as changes in those won't propagate.

Instead, if your store needs to refer to props (or useState based local state), sync those values into the store with useEffect.

Note that in many cases it is possible to extract the initializer function to a function outside the component definition. Which makes it possible to test the store itself in a more straight-forward manner, and avoids creating the initializer closure on each re-render.

Note: using useLocalObservable is mostly beneficial for really complex local state, or to obtain more uniform code base. Note that using a local store might conflict with future React features like concurrent rendering.

Server Side Rendering with enableStaticRendering

When using server side rendering, normal lifecycle hooks of React components are not fired, as the components are rendered only once. Since components are never unmounted, observer components would in this case leak memory when being rendered server side. To avoid leaking memory, call enableStaticRendering(true) when using server side rendering.

import { enableStaticRendering } from "mobx-react"

enableStaticRendering(true)

This makes sure the component won't try to react to any future data changes.

Which components should be marked with observer?

The simple rule of thumb is: all components that render observable data. If you don't want to mark a component as observer, for example to reduce the dependencies of a generic component package, make sure you only pass it plain data.

Enabling decorators (optional)

Decorators are currently a stage-2 ESNext feature. How to enable them is documented here.

Should I still use smart and dumb components?

See this thread. TL;DR: the conceptual distinction makes a lot of sense when using MobX as well, but use observer on all components.

DevTools

mobx-react@6 and higher are no longer compatible with the mobx-react-devtools. That is, the MobX react devtools will no longer show render timings or dependency trees of the component. The reason is that the standard React devtools are also capable of highlighting re-rendering components. And the dependency tree of a component can now be inspected by the standard devtools as well, as shown in the image below:

hooks.png

FAQ

Should I use observer for each component?

You should use observer on every component that displays observable data. Even the small ones. observer allows components to render independently from their parent and in general this means that the more you use observer, the better the performance become. The overhead of observer itself is negligible. See also Do child components need @observer?

I see React warnings about forceUpdate / setState from React

The following warning will appear if you trigger a re-rendering between instantiating and rendering a component:


Warning: forceUpdate(...): Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.`

-- or --


Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.

Usually this means that (another) component is trying to modify observables used by this components in their constructor or getInitialState methods. This violates the React Lifecycle, componentWillMount should be used instead if state needs to be modified before mounting.