A deeply reactive hook for MobX that efficiently tracks property access in React
components as an alternative to the
observer
HOC.
npm install use-observable-mobx
# or
yarn add use-observable-mobx
# or
pnpm add use-observable-mobx
- Deeply Reactive: Automatically tracks and reacts to all accessed properties, including nested objects and arrays
- Efficient Rendering: Components rerender only when accessed properties change
import { makeAutoObservable } from "mobx";
import { useState } from "react";
import { useObservable } from "use-observable-mobx";
interface Item {
id: number;
text: string;
}
class Store {
counter = 0;
items: Item[] = [{ id: 1, text: "Item 1" }];
constructor() {
makeAutoObservable(this);
}
increment() {
this.counter++;
}
addItem(text: string) {
this.items.push({
id: this.items.length + 1,
text,
});
}
}
const store = new Store();
const Counter = () => {
const { counter, increment } = useObservable(store);
return (
<div>
<p>Count: {counter}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
// Or compose hooks without needing to import the store again
const useStore = () => useObservable(store);
const ItemList = () => {
const { items, addItem } = useStore();
const [text, setText] = useState("");
return (
<div>
<ul>
{items.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button
onClick={() => {
addItem(text);
setText("");
}}
>
Add Item
</button>
</div>
);
};
Sometimes you need to access the original MobX object without the reactive proxy, such as when placing an object in React context or passing it to a function that expects the original object.
import { type PropsWithChildren, createContext } from "react";
import { useObservable } from "use-observable-mobx";
const ItemContext = createContext<Item | null>(null);
const useItemContext = () => {
const item = useContext(ItemContext);
if (!item) {
throw new Error("useItemContext must be used within an ItemProvider");
}
return useObservable(item);
};
const ItemProvider = ({
children,
item,
}: PropsWithChildren<{ item: Item }>) => (
// Unwrap when passing to context to keep the mobx object reference the same
<StoreContext.Provider value={useObservable.unwrap(item)}>
{children}
</StoreContext.Provider>
);
const Item = () => {
const item = useItemContext();
return <li>{item.text}</li>;
};
const ItemList = () => {
const { items, addItem } = useCounter();
const [text, setText] = useState("");
return (
<div>
<ul>
{items.map((item) => (
<ItemProvider key={item.id} item={item} />
))}
</ul>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button
onClick={() => {
addItem(text);
setText("");
}}
>
Add Item
</button>
</div>
);
};
import { isReactiveProxy } from "use-observable-mobx";
const MyComponent = () => {
const store = useObservable(myStore);
console.log(isReactiveProxy(store)); // true
console.log(isReactiveProxy({})); // false
};
The useObservable
hook creates a deeply reactive proxy that:
- Tracks all property access during render
- Subscribes to MobX observables for those properties
- Triggers re-renders when any accessed property changes
Unlike traditional MobX integration approaches, this hook doesn't require wrapping components in observers or using special syntax. It simply works by tracking what you actually use in your component.
This library was inspired by:
- Valtio: Thanks to Valtio for laying the groundwork for a deeply reactive approach that tracks property access in React components which this library borrows from.
- MobX:
Thanks to the MobX team for their
useObserver
implementation, which this hook borrows from extensively.
Thanks to Gavel for sponsoring the initial development.
MIT