[Deep Dive #28] Effect Lattices - discussion
We’ve ended up with multiple competing stores + route hints + local mirrors and our “keep them aligned” logic is spread across random effects. The deep dive’s answer is basically: embrace it as a lattice and make arbitration explicit.
Have you successfully treated effects as *policy* (arbitration rules) instead of as incidental sync? What cadence/guardrails keep a lattice from turning into a thrashy loop? How do you make the resolved truth visible in the UI so the route story is narratable?
Comments (10)
Back to latestIf you’re going to have multiple truths, you need one place where you decide which one wins. Otherwise you get accidental arbitration.
I like the framing because it forces ownership: each effect node owns one small rule.
The only way it stays reviewable is if the route renders the resolved truth (tab/density/banner/etc).
Related: State Management Best Practices and Routing and Navigation.
We kept it sane by making the resolved values explicit and rendering them:
tsx
const ledger = useLiveState('arb-ledger', { resolvedTab: 'overview', resolvedDensity: 'high', events: [] as any[] });
useRenderEffect(() => {
ledger.write('resolvedTab', hint.read('tab'));
ledger.write('events', [...ledger.read('events'), { at: Date.now(), kind: 'hint', value: hint.read('tab') }]);
});
return <div data-tab={ledger.read('resolvedTab')} data-density={ledger.read('resolvedDensity')} />;Counterpoint: an effect lattice is still an effect lattice.
If the rules are important, I’d rather move them into route state or a single arbitration service than encode them as a web of effects.
I get that. The post sold me mainly on “make it visible and cadenced”.
If you can see the resolved truth and the cadence, effects stop being spooky timing and start being a policy surface.
Visibility helps, for sure. I still think lattices need very strict “one rule per node” discipline.
Cadence is the part people skip. Without a cadence key, you’ll reconcile on every render and call it “reactive”.
With cadence, you can reason about when arbitration happens and why.
We had a thrash loop caused by two effects both “fixing” the same field in different stores.
It only became obvious once we rendered the resolved value and an event count on screen.
Guardrails matter. We used a “last sync” timestamp to avoid rapid loops:
tsx
const app = useVaultMirror('app');
const write = useVaultWrite('app');
useRenderEffect(() => {
const last = app.read('metrics.lastSync');
const now = Date.now();
if (now - last < 250) return;
write('metrics.lastSync', now);
});The “URL is allowed to lie” line is provocative but true in practice.
If you treat URL as hint, you need to render the resolved truth or users will feel gaslit by back/forward.
If you can’t explain why density/banner/tab resolved the way it did from a screenshot, the lattice is not operationally safe.
Effect lattices are basically “policy as effects”.
I can live with it if the route renders resolved values and the nodes are small and named.