diff --git a/.changeset/tall-bobcats-trade.md b/.changeset/tall-bobcats-trade.md
new file mode 100644
index 0000000000..0220b2d1ed
--- /dev/null
+++ b/.changeset/tall-bobcats-trade.md
@@ -0,0 +1,35 @@
+---
+'@xstate/react': patch
+'@xstate/svelte': patch
+'@xstate/vue': patch
+---
+
+The `useSelector(…)` hook from `@xstate/react` is now compatible with stores from `@xstate/store`.
+
+```tsx
+import { createStore } from '@xstate/store';
+import { useSelector } from '@xstate/react';
+
+const store = createStore(
+ {
+ count: 0
+ },
+ {
+ inc: {
+ count: (context) => context.count + 1
+ }
+ }
+);
+
+function Counter() {
+ // Note that this `useSelector` is from `@xstate/react`,
+ // not `@xstate/store/react`
+ const count = useSelector(store, (state) => state.context.count);
+
+ return (
+
+
+
+ );
+}
+```
diff --git a/packages/xstate-react/src/useSelector.ts b/packages/xstate-react/src/useSelector.ts
index e7ffcb7030..da73946f65 100644
--- a/packages/xstate-react/src/useSelector.ts
+++ b/packages/xstate-react/src/useSelector.ts
@@ -10,10 +10,15 @@ function defaultCompare(a: T, b: T) {
return a === b;
}
-export function useSelector(
+export function useSelector<
+ TActor extends Pick | undefined,
+ T
+>(
actor: TActor,
selector: (
- emitted: TActor extends AnyActorRef ? SnapshotFrom : undefined
+ snapshot: TActor extends { getSnapshot(): infer TSnapshot }
+ ? TSnapshot
+ : undefined
) => T,
compare: (a: T, b: T) => boolean = defaultCompare
): T {
diff --git a/packages/xstate-store/src/store.ts b/packages/xstate-store/src/store.ts
index f626d156d7..96d701379f 100644
--- a/packages/xstate-store/src/store.ts
+++ b/packages/xstate-store/src/store.ts
@@ -208,6 +208,12 @@ declare global {
}
}
+/**
+ * Creates a store function, which is a function that accepts the current snapshot and an event and returns a new snapshot.
+ * @param transitions
+ * @param updater
+ * @returns
+ */
export function createStoreTransition<
TContext extends StoreContext,
TEventPayloadMap extends EventPayloadMap
diff --git a/packages/xstate-store/test/UseActor.vue b/packages/xstate-store/test/UseActor.vue
new file mode 100644
index 0000000000..5e935de3e6
--- /dev/null
+++ b/packages/xstate-store/test/UseActor.vue
@@ -0,0 +1,28 @@
+
+ {{ snapshot.context.count }}
+
+
+
+
diff --git a/packages/xstate-store/test/UseActorRef.vue b/packages/xstate-store/test/UseActorRef.vue
new file mode 100644
index 0000000000..304ac41849
--- /dev/null
+++ b/packages/xstate-store/test/UseActorRef.vue
@@ -0,0 +1,29 @@
+
+ {{ count }}
+
+
+
+
diff --git a/packages/xstate-store/test/UseSelector.vue b/packages/xstate-store/test/UseSelector.vue
new file mode 100644
index 0000000000..493a8f0469
--- /dev/null
+++ b/packages/xstate-store/test/UseSelector.vue
@@ -0,0 +1,29 @@
+
+ {{ count }}
+
+
+
+
diff --git a/packages/xstate-store/test/react.test.tsx b/packages/xstate-store/test/react.test.tsx
index 3dd9c31fe1..b05dcba39f 100644
--- a/packages/xstate-store/test/react.test.tsx
+++ b/packages/xstate-store/test/react.test.tsx
@@ -1,6 +1,11 @@
import { fireEvent, screen, render } from '@testing-library/react';
-import { createStore } from '../src/index.ts';
+import { createStore, fromStore } from '../src/index.ts';
import { useSelector } from '../src/react.ts';
+import {
+ useActor,
+ useActorRef,
+ useSelector as useXStateSelector
+} from '@xstate/react';
import ReactDOM from 'react-dom';
it('useSelector should work', () => {
@@ -146,3 +151,118 @@ it('can batch updates', () => {
expect(countDiv.textContent).toEqual('2');
});
+
+it('useSelector (@xstate/react) should work', () => {
+ const store = createStore(
+ {
+ count: 0
+ },
+ {
+ inc: {
+ count: (ctx) => ctx.count + 1
+ }
+ }
+ );
+
+ const Counter = () => {
+ const count = useXStateSelector(store, (s) => s.context.count);
+
+ return (
+ {
+ store.send({ type: 'inc' });
+ }}
+ >
+ {count}
+
+ );
+ };
+
+ render();
+
+ const countDiv = screen.getByTestId('count');
+
+ expect(countDiv.textContent).toEqual('0');
+
+ fireEvent.click(countDiv);
+
+ expect(countDiv.textContent).toEqual('1');
+});
+
+it('useActor (@xstate/react) should work', () => {
+ const store = fromStore(
+ {
+ count: 0
+ },
+ {
+ inc: {
+ count: (ctx) => ctx.count + 1
+ }
+ }
+ );
+
+ const Counter = () => {
+ const [snapshot, send] = useActor(store);
+
+ return (
+ {
+ send({ type: 'inc' });
+ }}
+ >
+ {snapshot.context.count}
+
+ );
+ };
+
+ render();
+
+ const countDiv = screen.getByTestId('count');
+
+ expect(countDiv.textContent).toEqual('0');
+
+ fireEvent.click(countDiv);
+
+ expect(countDiv.textContent).toEqual('1');
+});
+
+it('useActorRef (@xstate/react) should work', () => {
+ const store = fromStore(
+ {
+ count: 0
+ },
+ {
+ inc: {
+ count: (ctx) => ctx.count + 1
+ }
+ }
+ );
+
+ const Counter = () => {
+ const actorRef = useActorRef(store);
+ const count = useXStateSelector(actorRef, (s) => s.context.count);
+
+ return (
+ {
+ actorRef.send({ type: 'inc' });
+ }}
+ >
+ {count}
+
+ );
+ };
+
+ render();
+
+ const countDiv = screen.getByTestId('count');
+
+ expect(countDiv.textContent).toEqual('0');
+
+ fireEvent.click(countDiv);
+
+ expect(countDiv.textContent).toEqual('1');
+});
diff --git a/packages/xstate-store/test/vue.test.ts b/packages/xstate-store/test/vue.test.ts
new file mode 100644
index 0000000000..585c441ba1
--- /dev/null
+++ b/packages/xstate-store/test/vue.test.ts
@@ -0,0 +1,34 @@
+import { fireEvent, render } from '@testing-library/vue';
+import UseSelector from './UseSelector.vue';
+import UseActor from './UseActor.vue';
+import UseActorRef from './UseActorRef.vue';
+
+it('works with `useSelector(…)` (@xstate/vue)', async () => {
+ const { getByTestId } = render(UseSelector);
+
+ const countEl = getByTestId('count');
+ const incrementEl = getByTestId('increment');
+
+ await fireEvent.click(incrementEl);
+ expect(countEl.textContent).toBe('1');
+});
+
+it('works with `useActor(…)` (@xstate/vue)', async () => {
+ const { getByTestId } = render(UseActor);
+
+ const countEl = getByTestId('count');
+ const incrementEl = getByTestId('increment');
+
+ await fireEvent.click(incrementEl);
+ expect(countEl.textContent).toBe('1');
+});
+
+it('works with `useActorRef(…)` (@xstate/vue)', async () => {
+ const { getByTestId } = render(UseActorRef);
+
+ const countEl = getByTestId('count');
+ const incrementEl = getByTestId('increment');
+
+ await fireEvent.click(incrementEl);
+ expect(countEl.textContent).toBe('1');
+});
diff --git a/packages/xstate-svelte/src/useSelector.ts b/packages/xstate-svelte/src/useSelector.ts
index 8441fe3566..a0c35b5b30 100644
--- a/packages/xstate-svelte/src/useSelector.ts
+++ b/packages/xstate-svelte/src/useSelector.ts
@@ -1,13 +1,20 @@
import { readable } from 'svelte/store';
-import type { ActorRef, SnapshotFrom, Subscription } from 'xstate';
+import type { ActorRef, AnyActorRef, SnapshotFrom, Subscription } from 'xstate';
function defaultCompare(a: T, b: T) {
return a === b;
}
-export const useSelector = , T>(
+export const useSelector = <
+ TActor extends Pick,
+ T
+>(
actor: TActor,
- selector: (snapshot: SnapshotFrom) => T,
+ selector: (
+ snapshot: TActor extends { getSnapshot(): infer TSnapshot }
+ ? TSnapshot
+ : undefined
+ ) => T,
compare: (a: T, b: T) => boolean = defaultCompare
) => {
let sub: Subscription;
diff --git a/packages/xstate-vue/src/useSelector.ts b/packages/xstate-vue/src/useSelector.ts
index 8900b9c130..aeb6f53b63 100644
--- a/packages/xstate-vue/src/useSelector.ts
+++ b/packages/xstate-vue/src/useSelector.ts
@@ -1,5 +1,5 @@
import { Ref, isRef, shallowRef, watch } from 'vue';
-import { ActorRef, SnapshotFrom } from 'xstate';
+import { AnyActorRef } from 'xstate';
function defaultCompare(a: T, b: T) {
return a === b;
@@ -9,11 +9,14 @@ const noop = () => {
/* ... */
};
-export function useSelector | undefined, T>(
+export function useSelector<
+ TActor extends Pick | undefined,
+ T
+>(
actor: TActor | Ref,
selector: (
- snapshot: TActor extends ActorRef
- ? SnapshotFrom
+ snapshot: TActor extends { getSnapshot(): infer TSnapshot }
+ ? TSnapshot
: undefined
) => T,
compare: (a: T, b: T) => boolean = defaultCompare