mobx-use-react
v0.1.1
Published
React hooks for explicit MobX observation without wrapping components in observer.
Maintainers
Readme
mobx-use-react
React hooks for explicit MobX observation without wrapping components in
observer.
mobx-use-react is designed for React 19+, MobX 6, web, and React Native. The
main entrypoint configures React batching automatically, matching the default
ergonomics of mobx-react-lite.
Installation
bun add mobx-use-react mobx reactFor web apps, install react-dom. For React Native apps, install
react-native.
useObservable
Read a single observable property:
import { useObservable } from "mobx-use-react";
function Counter({ store }: { store: { count: number } }) {
const count = useObservable(store, "count");
return <Text>{count}</Text>;
}Read a derived value:
function UserLabel({ user }: { user: UserStore }) {
const label = useObservable(() => `${user.firstName} ${user.lastName}`);
return <Text>{label}</Text>;
}Only observable reads inside useObservable are tracked. Reads elsewhere in the
component behave like normal JavaScript reads.
Usage Guide
MobX tracks the exact observable properties read by each useObservable call.
React still rerenders at component boundaries, so place subscriptions in the
smallest component that actually needs the value:
function UserRow({ user }: { user: UserStore }) {
return (
<View>
<UserName user={user} />
<UserPresence user={user} />
</View>
);
}
function UserName({ user }: { user: UserStore }) {
const name = useObservable(user, "name");
return <Text>{name}</Text>;
}
function UserPresence({ user }: { user: UserStore }) {
const isOnline = useObservable(user, "isOnline");
return <StatusDot online={isOnline} />;
}Pass observable objects down and dereference them late. Reading user.name in
UserName lets a name change rerender UserName without rerendering the parent
that passed user.
Use direct property reads for simple values:
const name = useObservable(user, "name");
const email = useObservable(user, "email");
const isOnline = useObservable(user, "isOnline");Use a selector when the component intentionally needs one derived or grouped view:
const displayName = useObservable(() => `${user.firstName} ${user.lastName}`);For large collections, keep list mapping in a dedicated component and track the list structure there. Use stable keys so React can preserve rows across inserts, removes, and reorders:
function UserPanel({
users,
session,
}: {
users: UserStore[];
session: SessionStore;
}) {
const title = useObservable(session, "title");
return (
<View>
<Text>{title}</Text>
<UserList users={users} />
</View>
);
}
function UserList({ users }: { users: UserStore[] }) {
const rowUsers = useObservable(() => users.slice());
return rowUsers.map((user) => <UserRow key={user.id} user={user} />);
}When passing data into a component that does not call useObservable, pass
plain values instead of expecting that child to track observables:
function UserBadge({ user }: { user: UserStore }) {
const name = useObservable(user, "name");
const avatarUrl = useObservable(user, "avatarUrl");
return <ThirdPartyBadge name={name} avatarUrl={avatarUrl} />;
}useObservableEffect
Use MobX tracking for side effects that should run when observables change, without requiring a React rerender:
import { useObservableEffect } from "mobx-use-react";
function UserSubscription({ store }: { store: SessionStore }) {
useObservableEffect(
() => store.userId,
(userId) => {
if (!userId) return;
const subscription = subscribeToUser(userId);
return () => {
subscription.close();
};
},
{ fireImmediately: true },
);
return null;
}useObservableEffect uses React for mount/unmount cleanup and MobX reaction
for observable tracking. Cleanup returned by the effect runs before the next
effect and on unmount.
If the selector captures React props or state that changes which observables are
tracked, pass deps:
useObservableEffect(
() => (showPrimary ? store.primaryUserId : store.secondaryUserId),
(userId) => {
analytics.identify(userId);
},
{ fireImmediately: true, deps: [showPrimary] },
);All MobX reaction options are supported, including fireImmediately, equals,
delay, scheduler, name, onError, and signal.
Static Rendering
For SSR-style rendering where hooks should read values without subscribing:
import { enableStaticRendering } from "mobx-use-react";
enableStaticRendering(true);Use isUsingStaticRendering() to inspect the current flag.
Batching
The main package configures MobX batching automatically:
import { useObservable } from "mobx-use-react";Web bundlers use react-dom batching. React Native bundlers can use the
package's react-native entry, which configures react-native batching.
MobX has one global reactionScheduler; if multiple libraries configure it, the
last configuration wins. This is compatible with mobx-react-lite: both
libraries configure MobX to use React's platform batch function, so importing
both is fine.
API
useObservable<TObject, TKey extends keyof TObject>(
source: TObject,
key: TKey,
): TObject[TKey];
useObservable<TValue>(selector: () => TValue): TValue;
useObservableEffect<TValue>(
selector: () => TValue,
effect: (
value: TValue,
previousValue: TValue,
reaction: IReactionPublic,
) => void | (() => void),
options?: UseObservableEffectOptions<TValue>,
): void;
enableStaticRendering(enable: boolean): void;
isUsingStaticRendering(): boolean;clearTimers() is exported for tests that need to flush abandoned-render cleanup
fallback timers.
