Repository: statelyai/xstate (ts)

Issue:
Bug: `createAsyncAtom` does not propagate status updates after promise resolves
### XState version

XState version 5

### Description

The async atom's internal status updates correctly after the promise resolves (from 'pending' to 'done' or 'error'), but subscribers are not notified about the change.





### Expected result

Subscribers and dependent computations should be notified when the status changes, just like with synchronous updates.

### Actual result

Dependent computations or subscriptions don't re-run when the status changes.

### Reproduction

https://stackblitz.com/edit/vitejs-vite-1owmrvoj?file=src%2Fmain.js

### Additional context

It looks like atom._update(...) is called when the promise resolves, but there’s no propagation of updates.

https://github.com/statelyai/xstate/blob/dcac5b2ae6baa1ca89e7fc3e0bb63449772911ab/packages/xstate-store/src/atom.ts#L71

Pull request:
[@xstate/store] Fix `createAsyncAtom` propagation
Fix: `createAsyncAtom` now properly propagates status updates after promise resolves/rejects.

Fixes #5327

Patch:
diff --git a/.changeset/bitter-drinks-cover.md b/.changeset/bitter-drinks-cover.md
deleted file mode 100644
index 6f822d8c06..0000000000
--- a/.changeset/bitter-drinks-cover.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-'@xstate/store': patch
----
-
-The XState Store undo/redo package can now be imported as `@xstate/store/undo`.
-
-```ts
-import { createStore } from '@xstate/store';
-import { undoRedo } from '@xstate/store/undo';
-
-const store = createStore(
-  undoRedo({
-    context: {
-      count: 0
-    },
-    on: {
-      // ...
-    }
-  })
-);
-
-// ...
-```
diff --git a/.changeset/dirty-donuts-clean.md b/.changeset/dirty-donuts-clean.md
new file mode 100644
index 0000000000..6e2285b52b
--- /dev/null
+++ b/.changeset/dirty-donuts-clean.md
@@ -0,0 +1,5 @@
+---
+'@xstate/store': patch
+---
+
+Fix: `createAsyncAtom` now properly propagates status updates after promise resolves/rejects.
diff --git a/packages/xstate-store/CHANGELOG.md b/packages/xstate-store/CHANGELOG.md
index 63a47866bf..a9fe326e67 100644
--- a/packages/xstate-store/CHANGELOG.md
+++ b/packages/xstate-store/CHANGELOG.md
@@ -1,11 +1,34 @@
 # @xstate/store
 
+## 3.8.1
+
+### Patch Changes
+
+- [#5326](https://github.com/statelyai/xstate/pull/5326) [`68ab6fb`](https://github.com/statelyai/xstate/commit/68ab6fb72d20c5bd2eb8d1d6249dc3046da79010) Thanks [@davidkpiano](https://github.com/davidkpiano)! - The XState Store undo/redo package can now be imported as `@xstate/store/undo`.
+
+  ```ts
+  import { createStore } from '@xstate/store';
+  import { undoRedo } from '@xstate/store/undo';
+
+  const store = createStore(
+    undoRedo({
+      context: {
+        count: 0
+      },
+      on: {
+        // ...
+      }
+    })
+  );
+
+  // ...
+  ```
+
 ## 3.8.0
 
 ### Minor Changes
 
 - [#5305](https://github.com/statelyai/xstate/pull/5305) [`725530f`](https://github.com/statelyai/xstate/commit/725530fd462c4300319fad82efc545ff44cf3e22) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Added undo/redo functionality to XState Store via the `undoRedo` higher-order store logic:
-
   - Adds `undo` and `redo` events to stores
   - Supports grouping related events into transactions using `transactionId`
   - Maintains event history for precise state reconstruction
@@ -121,7 +144,6 @@
 ### Minor Changes
 
 - [#5250](https://github.com/statelyai/xstate/pull/5250) [`a1bffb55b2029bde82e542d5936c51d961909a37`](https://github.com/statelyai/xstate/commit/a1bffb55b2029bde82e542d5936c51d961909a37) Thanks [@davidkpiano](https://github.com/davidkpiano)! - - Improved atom architecture with better dependency management (the diamond problem is solved!)
-
   - Optimized recomputation logic to prevent unnecessary updates
   - Added support for custom equality functions through `compare` option in `createAtom`, allowing fine-grained control over when atoms update:
 
@@ -174,7 +196,6 @@
 ### Minor Changes
 
 - [#5221](https://github.com/statelyai/xstate/pull/5221) [`4635d3d8d3debcfeef5cddd78613e32891c10eac`](https://github.com/statelyai/xstate/commit/4635d3d8d3debcfeef5cddd78613e32891c10eac) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Added `createAtom()` for creating reactive atoms that can be combined with other atoms and stores:
-
   - Create simple atoms with initial values:
 
     ```ts
@@ -245,7 +266,6 @@
 ### Minor Changes
 
 - [#5200](https://github.com/statelyai/xstate/pull/5200) [`0332a16a42fb372eb614df74ff4cb7f003c31fc8`](https://github.com/statelyai/xstate/commit/0332a16a42fb372eb614df74ff4cb7f003c31fc8) Thanks [@{](https://github.com/{)! - Added selectors to @xstate/store that enable efficient state selection and subscription:
-
   - `store.select(selector)` function to create a "selector" entity where you can:
     - Get current value with `.get()`
     - Subscribe to changes with `.subscribe(callback)`
diff --git a/packages/xstate-store/package.json b/packages/xstate-store/package.json
index 1535b1d167..fddbfa1db8 100644
--- a/packages/xstate-store/package.json
+++ b/packages/xstate-store/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@xstate/store",
-  "version": "3.8.0",
+  "version": "3.8.1",
   "description": "Simple stores",
   "keywords": [
     "store",
diff --git a/packages/xstate-store/src/atom.ts b/packages/xstate-store/src/atom.ts
index 4df6781717..609ef9559e 100644
--- a/packages/xstate-store/src/atom.ts
+++ b/packages/xstate-store/src/atom.ts
@@ -68,10 +68,24 @@ export function createAsyncAtom<T>(
   const atom = createAtom<AsyncAtomState<T>>(() => {
     getValue().then(
       (data) => {
-        atom._update({ status: 'done', data });
+        if (atom._update({ status: 'done', data })) {
+          const subs = atom._subs;
+          if (subs !== undefined) {
+            propagate(subs);
+            shallowPropagate(subs);
+            flush();
+          }
+        }
       },
       (error) => {
-        atom._update({ status: 'error', error });
+        if (atom._update({ status: 'error', error })) {
+          const subs = atom._subs;
+          if (subs !== undefined) {
+            propagate(subs);
+            shallowPropagate(subs);
+            flush();
+          }
+        }
       }
     );
 


Test patch:
diff --git a/packages/xstate-store/test/atom.test.ts b/packages/xstate-store/test/atom.test.ts
index c64b195a4e..d3856c9c61 100644
--- a/packages/xstate-store/test/atom.test.ts
+++ b/packages/xstate-store/test/atom.test.ts
@@ -699,4 +699,66 @@ describe('async atoms', () => {
 
     expect('set' in atom).toBe(false);
   });
+
+  it('should notify subscribers when async operation completes successfully', async () => {
+    const log = vi.fn();
+    const atom = createAsyncAtom(async () => {
+      await new Promise((resolve) => setTimeout(resolve, 10));
+      return 'test-value';
+    });
+
+    atom.subscribe(log);
+
+    expect(atom.get()).toEqual({ status: 'pending' });
+    expect(log).not.toHaveBeenCalled();
+
+    await new Promise((resolve) => setTimeout(resolve, 20));
+
+    expect(log).toHaveBeenCalledTimes(1);
+    expect(log).toHaveBeenCalledWith({ status: 'done', data: 'test-value' });
+    expect(atom.get()).toEqual({ status: 'done', data: 'test-value' });
+  });
+
+  it('should notify subscribers when async operation fails', async () => {
+    const log = vi.fn();
+    const error = new Error('test error');
+    const atom = createAsyncAtom(async () => {
+      await new Promise((resolve) => setTimeout(resolve, 10));
+      throw error;
+    });
+
+    atom.subscribe(log);
+
+    expect(atom.get()).toEqual({ status: 'pending' });
+    expect(log).not.toHaveBeenCalled();
+
+    await new Promise((resolve) => setTimeout(resolve, 20));
+
+    expect(log).toHaveBeenCalledTimes(1);
+    expect(log).toHaveBeenCalledWith({ status: 'error', error });
+    expect(atom.get()).toEqual({ status: 'error', error });
+  });
+
+  it('should notify multiple subscribers when async operation completes', async () => {
+    const log1 = vi.fn();
+    const log2 = vi.fn();
+    const atom = createAsyncAtom(async () => {
+      await new Promise((resolve) => setTimeout(resolve, 10));
+      return 'multi-test';
+    });
+
+    atom.subscribe(log1);
+    atom.subscribe(log2);
+
+    expect(atom.get()).toEqual({ status: 'pending' });
+    expect(log1).not.toHaveBeenCalled();
+    expect(log2).not.toHaveBeenCalled();
+
+    await new Promise((resolve) => setTimeout(resolve, 20));
+
+    expect(log1).toHaveBeenCalledTimes(1);
+    expect(log1).toHaveBeenCalledWith({ status: 'done', data: 'multi-test' });
+    expect(log2).toHaveBeenCalledTimes(1);
+    expect(log2).toHaveBeenCalledWith({ status: 'done', data: 'multi-test' });
+  });
 });