mrkeyoor.com_
Sat 08 Aug 21:58 UTC
npmWeb Frontendupdated 08 Aug 2026

mobx-react-lite

mobx-react-lite is the function-component bridge between React and MobX. Its observer wrapper records which observable values a component reads during rendering and subscribes that component to just those values; Observer does the same for an inline render region, and useLocalObservable creates component-owned MobX state. The package is not MobX itself: version 5 expects MobX 7 as a peer, along with React 18 or 19. It deliberately excludes class-component bindings and legacy Provider/inject APIs.

Verdict

The right React binding when MobX 7 is already an intentional architecture choice and the UI is function-component based. Do not add it just to manage a few local flags, and do not upgrade to version 5 until React 18+, native Proxy support, and removed legacy APIs are accounted for.

API stability3/5The central observer API has stayed recognizable for years, and the version 5 public surface is intentionally narrow: observer, Observer, useLocalObservable, enableStaticRendering, and isUsingStaticRendering. The major upgrade is still substantial. Version 5 requires MobX 7 and React 18+, removes useObserver, useLocalStore, useAsObservableSource, Provider, inject, batching utilities, and the old static-rendering name, while MobX 7 removes non-Proxy mode and legacy decorators.
Docs5/5The package README has a compatibility table, a concise v5 removal list, SSR instructions, test-timer guidance, and exact descriptions for every supported binding. The longer React integration guide explains late dereferencing, Context, local state tradeoffs, third-party component boundaries, callbacks with Observer, HOC order, props used by computed values, autorun cleanup, named observer functions, ESLint hook detection, and React DevTools names. The changelog gives direct replacements for removed APIs.
Maintenance5/5Version 5.0.0 was published on July 30, 2026 as part of the coordinated MobX 7 release, and the monorepo was pushed on August 2. GitHub reports 67 open issues and pull requests across the full MobX project, not just this binding. The release updated React support, removed stale batching paths, reduced the modern runtime surface, and documented migration replacements. The shared monorepo also keeps core and React compatibility changes in one release process.
Ecosystem5/5The measured week recorded 3,660,866 downloads, the MobX repository has 28,205 stars, and the binding supports React 18, React 19, SSR, and React Native through its export map. MobX has established tools for computed values, reactions, dependency inspection, spying, Context stores, and class-component bindings through mobx-react. The cost of that mature ecosystem is commitment to MobX's observable model; integrations expecting immutable snapshots often need explicit conversion with toJS or plain projection.

Use it if

  • Your application already models domain state with MobX 7 and needs fine-grained React subscriptions without handwritten selectors
  • You use React function components and want observable reads during render to define dependencies automatically
  • You have deep, computed, or shared mutable domain objects that would be awkward to normalize into component state
  • You need a small React binding layer with Context integration, SSR static rendering, and React Native exports
Skip it if

Setup reality

Install both peers explicitly with npm install mobx@^7 mobx-react-lite@^5. Version 5 supports only React 18 and 19, and only modern runtimes with native Proxy. There are no native addons or configuration files, but wrapping components correctly is the real setup. Every component that directly reads observables needs observer; wrapping only a parent does not make child reads reactive. Read properties as late as possible inside the observer component, and convert observable arrays or objects to plain data before handing them to a third-party component that is not an observer. observer already applies React memo, so adding memo is redundant. Use named functions inside observer: the project docs warn that eslint-plugin-react-hooks may not recognize an inline arrow wrapped by observer, and names also improve React DevTools output. For SSR, call enableStaticRendering(true) in the server environment so reactions do not remain subscribed after the render. In tests on runtimes without FinalizationRegistry, the package may use cleanup timers for aborted React renders; its README provides clearTimers for afterEach when fake-timer or open-handle checks hang. Version 5 removes useObserver, useLocalStore, useAsObservableSource, useStaticRendering, Provider, inject, and batching helpers. Migrations must replace those APIs rather than relying on aliases. React Context is the supported dependency-injection path, and Provider values should usually keep the same store identity while the store mutates internally.

Patterns

Re-render from observable values read during renderobserve-component

import { makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react-lite';

class CounterStore {
  count = 0;
  constructor() { makeAutoObservable(this); }
  increment() { this.count += 1; }
}

const counter = new CounterStore();

export const Counter = observer(function Counter() {
  return <button onClick={() => counter.increment()}>{counter.count}</button>;
});

Use a named function inside observer so React DevTools and eslint-plugin-react-hooks can identify the component.

Pass a store object and dereference it latepass-observable-prop

import { observer } from 'mobx-react-lite';

type Props = { todo: { title: string; done: boolean; toggle(): void } };

export const TodoRow = observer(function TodoRow({ todo }: Props) {
  return (
    <label>
      <input type="checkbox" checked={todo.done} onChange={() => todo.toggle()} />
      {todo.title}
    </label>
  );
});

Pass the observable object, not todo.title and todo.done read by a non-observer parent. observer tracks values read inside its own render only.

Share a stable store through React Contextprovide-store-with-context

import { createContext, useContext } from 'react';
import { observer } from 'mobx-react-lite';

const StoreContext = createContext<RootStore | null>(null);
export const StoreProvider = ({ store, children }: { store: RootStore; children: React.ReactNode }) => (
  <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
function useRootStore() {
  const store = useContext(StoreContext);
  if (!store) throw new Error('StoreProvider is missing');
  return store;
}
export const UserName = observer(function UserName() {
  return <span>{useRootStore().session.userName}</span>;
});

Keep the Provider value identity stable and mutate observable fields on the store. Version 5 does not provide the old MobX Provider or inject APIs.

Create deep local state with useLocalObservablecreate-local-observable

import { observer, useLocalObservable } from 'mobx-react-lite';

export const Quantity = observer(function Quantity() {
  const state = useLocalObservable(() => ({
    value: 1,
    get doubled() { return this.value * 2; },
    increment() { this.value += 1; },
  }));
  return <button onClick={state.increment}>{state.value} / {state.doubled}</button>;
});

Prefer React useState for shallow UI-only state. The docs recommend local observables when state is deep, computed, or shared with observer children.

Synchronize a prop used by a local computed valuesync-prop-to-computed

import { useEffect } from 'react';
import { observer, useLocalObservable } from 'mobx-react-lite';

export const OffsetTotal = observer(function OffsetTotal({ offset }: { offset: number }) {
  const state = useLocalObservable(() => ({
    total: 0,
    offset,
    setOffset(value: number) { this.offset = value; },
    get adjusted() { return this.total - this.offset; },
  }));
  useEffect(() => state.setOffset(offset), [offset, state]);
  return <output>{state.adjusted}</output>;
});

React props are not observable. A computed created once by useLocalObservable will not see later prop values unless they are synchronized or read directly during render.

Track observables inside a third-party render callbackobserve-render-callback

import { Observer, observer } from 'mobx-react-lite';

export const GridCell = observer(function GridCell({ todo }: { todo: Todo }) {
  return (
    <Grid
      renderCell={() => (
        <Observer>{() => <span>{todo.title}</span>}</Observer>
      )}
    />
  );
});

The callback executes during Grid's render, outside GridCell's tracked render. Observer creates a tracked region at the actual read site.

Give a non-observer component a plain snapshotpass-plain-data

import { toJS } from 'mobx';
import { observer } from 'mobx-react-lite';

export const Results = observer(function Results({ store }: { store: SearchStore }) {
  const rows = toJS(store.results);
  return <ThirdPartyGrid rows={rows} />;
});

A third-party component that is not observer will not subscribe to observable arrays or nested fields. Explicit projection is often cheaper than converting a large graph with toJS.

Disable reactive subscriptions during SSRenable-server-rendering

import { enableStaticRendering } from 'mobx-react-lite';

enableStaticRendering(typeof window === 'undefined');

Run this during environment initialization. The README warns that observer subscriptions during server rendering can otherwise create garbage-collection problems.

Dispose an autorun when a component unmountsbind-autorun-to-lifecycle

import { autorun } from 'mobx';
import { useEffect } from 'react';
import { observer } from 'mobx-react-lite';

export const DraftSaver = observer(function DraftSaver({ draft }: { draft: DraftStore }) {
  useEffect(() => autorun(() => {
    localStorage.setItem('draft', draft.text);
  }), [draft]);
  return <textarea value={draft.text} onChange={(e) => draft.setText(e.target.value)} />;
});

autorun returns its disposer, and returning it directly from useEffect ties the MobX reaction to the React component lifecycle.

Wrap a forwardRef component in the supported orderobserve-forwarded-ref

import { forwardRef } from 'react';
import { observer } from 'mobx-react-lite';

type Props = { form: FormStore };

export const NameInput = observer(forwardRef<HTMLInputElement, Props>(
  function NameInput({ form }, ref) {
    return <input ref={ref} value={form.name} onChange={(e) => form.setName(e.target.value)} />;
  },
));

Version 5 removed observer(fn, { forwardRef: true }). Create the forwardRef component first, then pass it to observer.

Keep observer as the innermost wrappercombine-higher-order-components

import { observer } from 'mobx-react-lite';

const ReactiveProfile = observer(function Profile({ store }: { store: ProfileStore }) {
  return <h1>{store.displayName}</h1>;
});

export const Profile = withErrorBoundary(ReactiveProfile);

The React guide says observer should be applied first when combining higher-order components; an outer observer may not see observable reads hidden inside another wrapper.

Clean fallback reaction timers in testsclear-test-timers

import { afterEach } from 'vitest';
import { clearTimers } from 'mobx-react-lite';

afterEach(() => {
  clearTimers();
});

This is mainly for environments without FinalizationRegistry where cleanup timers can keep Jest or Vitest open-handle checks alive after aborted renders.

Alternatives

PackageRegistryPick it when
zustandnpmChoose it for small hook-based stores with explicit selectors and no observable class model.
jotainpmChoose it when state is easier to compose as independent atoms with React-first dependency tracking.
valtionpmChoose it for mutable Proxy state with a smaller conceptual surface and snapshot-based React reads.
mobx-reactnpmChoose it when the same MobX application must observe React class components or use the Stage 3 observer decorator.