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.
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.
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
- Your app still has class components that must observe state: the package README says function components only and directs class users to mobx-react
- You cannot move to MobX 7 and React 18 or 19: version 5 declares those peer ranges and its compatibility table maps older MobX lines to older binding majors
- You target IE, an embedded WebView without native Proxy, or a non-Proxy React Native environment: MobX 7 removed its ES5 fallback and always uses Proxy-backed objects and arrays
- Most of the state is local loading flags, selections, or other shallow UI state: the React guide recommends useState first because local observables can limit some Suspense behavior
- Your team wants data dependencies visible as explicit selectors and immutable updates: observer discovers reads at runtime, so the convenience comes with a MobX-specific mutation and debugging model
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
| Package | Registry | Pick it when |
|---|---|---|
| zustand | npm | Choose it for small hook-based stores with explicit selectors and no observable class model. |
| jotai | npm | Choose it when state is easier to compose as independent atoms with React-first dependency tracking. |
| valtio | npm | Choose it for mutable Proxy state with a smaller conceptual surface and snapshot-based React reads. |
| mobx-react | npm | Choose it when the same MobX application must observe React class components or use the Stage 3 observer decorator. |