Understanding Jotai Store Subscriptions
•vibekōdo•3 min read
TL;DR
store.set(atom, value)
is synchronous. It completes only after Jotai has written the value, recomputed all invalidated atoms and synchronously invoked every relevant subscriber.- A subscriber fires only when
Object.is(prev, next)
isfalse
. - Derived atoms re-emit only when their computed value differs, not merely because their source atom changed.
- If your subscriber does expensive work, you are the bottleneck—kick that work to a micro-task or worker if the UI must stay fluid.
1. A 60-second refresher on Jotai internals
flowchart TD
A[store.set] --> B(writeAtomState)
B --> C(recomputeInvalidatedAtoms)
C --> D(flushCallbacks)
D -->|iterates| E[subscribers]
The hard truth: no await
anywhere. Every step runs inline.
If a subscriber's callback is slow, you feel it right inside your state write.
2. Equality check = Object.is
Jotai doesn't do deep compares, JSON stringification, etc. It's exactly:
const shouldNotify = !Object.is(oldValue, newValue);
Implications:
Type | Example that fires | Example that doesn't fire |
---|---|---|
string | 'a' -> 'b' |
'a' -> 'a' |
number | 1 -> 2 |
2 -> 2 |
object reference | {} -> {} (new ref) |
sameRef -> sameRef |
NaN |
1 -> NaN |
NaN -> NaN |
3. Derived atoms – nothing mystical
Take a base atom and a derived atom:
const countAtom = atom(0);
const doubleAtom = atom((get) => get(countAtom) * 2);
Scenario | double value | Fires? |
---|---|---|
set(countAtom, 1) |
2 | ✅ |
set(countAtom, 1) |
2 | ❌ (same) |
set(countAtom, 2) |
4 | ✅ |
The derived atom recomputes every time countAtom changes, but it only notifies when the result actually differs.
Property selectors
const objAtom = atom({ foo: 1, bar: 1 });
const fooAtom = atom((get) => get(objAtom).foo);
Write | objAtom fires | fooAtom fires |
---|---|---|
{ foo: 1, bar: 2 } (bar changed) |
✅ | ❌ (foo same) |
{ foo: 2, bar: 2 } (foo changed) |
✅ | ✅ |
📍 Mutable update trap
const ref = store.get(objAtom);
ref.foo = 99;
store.set(objAtom, ref); // same reference → no fire at all
Always use immutable updates when working with reference types.
4. store.set
and long-running subscribers
store.set
won't yield control until all subscribers finish their synchronous work.
store.sub(atom, () => {
heavyCPU(); // blocks!
queueMicrotask(lightIO); // executes after store.set returns
});
If you must run heavy logic:
store.sub(atom, () => {
setTimeout(heavyCPU, 0); // completely detach
});
or offload to a worker, or process inside React with useEffect
.
5. Takeaways
- Synchronous mindset – A Jotai write is like a Redux dispatch; treat it as blocking.
- Immutable updates – they're the key for selective re-rendering and derived-atom hygiene.
- Measure & offload – heavy subscribers will tank performance; use async boundaries wisely.
- Backed by tests – automated specs help you trust these mechanics (and future-proof against library updates).
Happy atom-crafting! 🧪