[@xstate/store] Fix synchronous updates to atom dependencies during a subscription callback#5513
Conversation
I don’t know that this situation is actually possible but I was concerned that calling `.get()` again might’ve introduced this behavior so I added a test for it. Even though it didn’t infinite loop before it seems like a reasonable test to keep around.
🦋 Changeset detectedLatest commit: 039f256 The changes in this PR will be included in the next version bump. This PR includes changesets to release 7 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
So I realized this fix is incomplete (it works but doesn't catch all cases) 🤦♂️ When I worked on the original fix I was wondering if a pending check was necessary but couldn't figure out a situation that would actually trigger it. Well I accidentally ran into it earlier today after some code restructuring and it took a bit to turn into a reproduction. If you have a subscription running inside another subscription through multiple levels of atoms an atom can be marked as pending but not dirty. This means the following updated test will fail currently: it('allows updates to a computed dependency during a subscription callback', () => {
const atom = createAtom({ a: 0, b: 0, c: 0 });
const a0 = createAtom(() => atom.get().a);
const a1 = createAtom(() => a0.get());
a1.subscribe(() => atom.set((ctx) => ({ ...ctx, b: ctx.a })));
const b0 = createAtom(() => atom.get().b);
const b1 = createAtom(() => b0.get());
b1.subscribe(() => atom.set((ctx) => ({ ...ctx, c: ctx.b })));
atom.set((ctx) => ({ ...ctx, a: ctx.a + 1 }));
atom.set((ctx) => ({ ...ctx, a: ctx.a + 1 }));
atom.set((ctx) => ({ ...ctx, a: ctx.a + 1 }));
expect(a0.get()).toBe(3);
expect(a1.get()).toBe(3);
expect(b0.get()).toBe(3);
expect(b1.get()).toBe(3);
expect(atom.get().a).toBe(3);
expect(atom.get().b).toBe(3);
expect(atom.get().c).toBe(3);
});The fix is pretty simple: add the pending flag check or drop the if entirely. I can work around it pretty easily (a second empty subscription is enough) but gonna think over it some to see if there isn't a better fix / design — may open a PR tomorrow 👍 |
|
@thecrypticace No worries, would be happy to review the next PR. |
Fix computed
atom.subscribe()callbacks not re-running after one of it's dependencies is synchronously updated inside the callback. Previously, callingsomeDependency.set()inside a subscription callback would prevent that subscription from being notified of future changes. This also affected store selector subscriptions which triggered an update to the store (b/c they're also just computed atoms).I ran into this when triggering an update to a store from a selector subscription that tracked cursor position and recorded (possibly) hovered items separately. I probably should've been using store events to trigger the secondary update but was trying to prototype things rather quickly
I found some workarounds while working on the fix:
queueMicrotaskatom.get()inside the subscription callback after updating the dependent atomThis is what the fix does internally if the atom ends up dirty after the observer is notified.
The extra subscription causes
atom.get()to get called again which cleans up after the first one's updates.