From 49cf95924cc58f1631bd4d6f34fb381031e07e5a Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 16 Jan 2025 01:04:57 -0500 Subject: [PATCH 01/15] Only complete assigners are allowed --- .changeset/thick-paws-invite.md | 5 ++ packages/xstate-store/src/store.ts | 90 +++++++------------- packages/xstate-store/src/types.ts | 2 +- packages/xstate-store/test/fromStore.test.ts | 9 +- packages/xstate-store/test/react.test.tsx | 49 ++++++----- packages/xstate-store/test/solid.test.tsx | 20 ++++- packages/xstate-store/test/store.test.ts | 33 +++---- packages/xstate-store/test/types.test.tsx | 50 +++++------ 8 files changed, 124 insertions(+), 134 deletions(-) create mode 100644 .changeset/thick-paws-invite.md diff --git a/.changeset/thick-paws-invite.md b/.changeset/thick-paws-invite.md new file mode 100644 index 0000000000..73f1274018 --- /dev/null +++ b/.changeset/thick-paws-invite.md @@ -0,0 +1,5 @@ +--- +'@xstate/store': major +--- + +Only complete assigners can now be used diff --git a/packages/xstate-store/src/store.ts b/packages/xstate-store/src/store.ts index 6a651d58be..89c13ab3d0 100644 --- a/packages/xstate-store/src/store.ts +++ b/packages/xstate-store/src/store.ts @@ -9,11 +9,8 @@ import { Recipe, Store, StoreAssigner, - StoreCompleteAssigner, StoreContext, StoreInspectionEvent, - StorePartialAssigner, - StorePropertyAssigner, StoreSnapshot } from './types'; @@ -57,17 +54,11 @@ function createStoreCore< >( initialContext: TContext, transitions: { - [K in keyof TEventPayloadMap & string]: - | StoreAssigner< - NoInfer, - { type: K } & TEventPayloadMap[K], - TEmitted - > - | StorePropertyAssigner< - NoInfer, - { type: K } & TEventPayloadMap[K], - TEmitted - >; + [K in keyof TEventPayloadMap & string]: StoreAssigner< + NoInfer, + { type: K } & TEventPayloadMap[K], + TEmitted + >; }, updater?: ( context: NoInfer, @@ -207,21 +198,13 @@ export type TransitionsFromEventPayloadMap< TContext extends StoreContext, TEmitted extends EventObject > = { - [K in keyof TEventPayloadMap & string]: - | StoreAssigner< - TContext, - { - type: K; - } & TEventPayloadMap[K], - TEmitted - > - | StorePropertyAssigner< - TContext, - { - type: K; - } & TEventPayloadMap[K], - TEmitted - >; + [K in keyof TEventPayloadMap & string]: StoreAssigner< + TContext, + { + type: K; + } & TEventPayloadMap[K], + TEmitted + >; }; /** @@ -264,17 +247,11 @@ export function createStore< }: { context: TContext; on: { - [K in keyof TEventPayloadMap & string]: - | StoreAssigner< - NoInfer, - { type: K } & TEventPayloadMap[K], - Cast - > - | StorePropertyAssigner< - NoInfer, - { type: K } & TEventPayloadMap[K], - Cast - >; + [K in keyof TEventPayloadMap & string]: StoreAssigner< + NoInfer, + { type: K } & TEventPayloadMap[K], + Cast + >; }; } & { types?: TTypes }): Store< TContext, @@ -445,13 +422,11 @@ export function createStoreTransition< TEmitted extends EventObject >( transitions: { - [K in keyof TEventPayloadMap & string]: - | StoreAssigner - | StorePropertyAssigner< - TContext, - { type: K } & TEventPayloadMap[K], - TEmitted - >; + [K in keyof TEventPayloadMap & string]: StoreAssigner< + TContext, + { type: K } & TEventPayloadMap[K], + TEmitted + >; }, updater?: ( context: TContext, @@ -480,9 +455,11 @@ export function createStoreTransition< if (typeof assigner === 'function') { currentContext = updater ? updater(currentContext, (draftContext) => - ( - assigner as StoreCompleteAssigner - )?.(draftContext, event, enqueue) + (assigner as StoreAssigner)?.( + draftContext, + event, + enqueue + ) ) : setter(currentContext, (draftContext) => Object.assign( @@ -501,14 +478,11 @@ export function createStoreTransition< const propAssignment = assigner[key]; partialUpdate[key] = typeof propAssignment === 'function' - ? ( - propAssignment as StorePartialAssigner< - TContext, - StoreEvent, - typeof key, - TEmitted - > - )(currentContext, event, enqueue) + ? (propAssignment as StoreAssigner)( + currentContext, + event, + enqueue + ) : propAssignment; } currentContext = Object.assign({}, currentContext, partialUpdate); diff --git a/packages/xstate-store/src/types.ts b/packages/xstate-store/src/types.ts index 23b90758b8..3dae424ae7 100644 --- a/packages/xstate-store/src/types.ts +++ b/packages/xstate-store/src/types.ts @@ -18,7 +18,7 @@ export type StoreAssigner< context: TContext, event: TEvent, enq: EnqueueObject -) => Partial; +) => TContext; export type StoreCompleteAssigner< TContext, TEvent extends EventObject, diff --git a/packages/xstate-store/test/fromStore.test.ts b/packages/xstate-store/test/fromStore.test.ts index 0f6cc1d75d..d1d7e2349e 100644 --- a/packages/xstate-store/test/fromStore.test.ts +++ b/packages/xstate-store/test/fromStore.test.ts @@ -4,11 +4,10 @@ import { fromStore } from '../src/index.ts'; describe('fromStore', () => { it('creates an actor from store logic with input (2 args)', () => { const storeLogic = fromStore((count: number) => ({ count }), { - inc: { - count: (ctx, ev: { by: number }) => { - return ctx.count + ev.by; - } - } + inc: (ctx, ev: { by: number }) => ({ + ...ctx, + count: ctx.count + ev.by + }) }); const actor = createActor(storeLogic, { diff --git a/packages/xstate-store/test/react.test.tsx b/packages/xstate-store/test/react.test.tsx index b05dcba39f..889c025ef5 100644 --- a/packages/xstate-store/test/react.test.tsx +++ b/packages/xstate-store/test/react.test.tsx @@ -14,9 +14,10 @@ it('useSelector should work', () => { count: 0 }, { - inc: { - count: (ctx) => ctx.count + 1 - } + inc: (ctx) => ({ + ...ctx, + count: ctx.count + 1 + }) } ); @@ -52,12 +53,14 @@ it('useSelector can take in a custom comparator', () => { items: [1, 2] }, { - same: { - items: () => [1, 2] // different array, same items - }, - different: { - items: () => [3, 4] - } + same: (ctx) => ({ + ...ctx, + items: [1, 2] // different array, same items + }), + different: (ctx) => ({ + ...ctx, + items: [3, 4] + }) } ); @@ -117,9 +120,10 @@ it('can batch updates', () => { count: 0 }, { - inc: { - count: (ctx) => ctx.count + 1 - } + inc: (ctx) => ({ + ...ctx, + count: ctx.count + 1 + }) } ); @@ -158,9 +162,10 @@ it('useSelector (@xstate/react) should work', () => { count: 0 }, { - inc: { - count: (ctx) => ctx.count + 1 - } + inc: (ctx) => ({ + ...ctx, + count: ctx.count + 1 + }) } ); @@ -196,9 +201,10 @@ it('useActor (@xstate/react) should work', () => { count: 0 }, { - inc: { - count: (ctx) => ctx.count + 1 - } + inc: (ctx) => ({ + ...ctx, + count: ctx.count + 1 + }) } ); @@ -234,9 +240,10 @@ it('useActorRef (@xstate/react) should work', () => { count: 0 }, { - inc: { - count: (ctx) => ctx.count + 1 - } + inc: (ctx) => ({ + ...ctx, + count: ctx.count + 1 + }) } ); diff --git a/packages/xstate-store/test/solid.test.tsx b/packages/xstate-store/test/solid.test.tsx index f690da0a91..fd94bd5a00 100644 --- a/packages/xstate-store/test/solid.test.tsx +++ b/packages/xstate-store/test/solid.test.tsx @@ -22,8 +22,14 @@ const createCounterStore = () => createStore( { count: 0, other: 0 }, { - increment: { count: ({ count }) => count + 1 }, - other: { other: ({ other }) => other + 1 } + increment: (ctx) => ({ + ...ctx, + count: ctx.count + 1 + }), + other: (ctx) => ({ + ...ctx, + other: ctx.other + 1 + }) } ); @@ -75,8 +81,14 @@ describe('Solid.js integration', () => { const store = createStore( { items: INITIAL_ITEMS }, { - same: { items: () => [...INITIAL_ITEMS] }, - different: { items: () => DIFFERENT_ITEMS } + same: (ctx) => ({ + ...ctx, + items: [...INITIAL_ITEMS] + }), + different: (ctx) => ({ + ...ctx, + items: DIFFERENT_ITEMS + }) } ); diff --git a/packages/xstate-store/test/store.test.ts b/packages/xstate-store/test/store.test.ts index 2e5db1bd07..aa270547aa 100644 --- a/packages/xstate-store/test/store.test.ts +++ b/packages/xstate-store/test/store.test.ts @@ -27,13 +27,14 @@ it('can update context with a property assigner', () => { const store = createStore( { count: 0, greeting: 'hello' }, { - inc: { - count: (ctx) => ctx.count + 1 - }, - updateBoth: { - count: () => 42, + inc: (ctx) => ({ + ...ctx, + count: ctx.count + 1 + }), + updateBoth: (ctx) => ({ + count: 42, greeting: 'hi' - } + }) } ); @@ -52,9 +53,9 @@ it('handles unknown events (does not do anything)', () => { const store = createStore( { count: 0 }, { - inc: { - count: (ctx) => ctx.count + 1 - } + inc: (ctx) => ({ + count: ctx.count + 1 + }) } ); @@ -165,7 +166,7 @@ it('createStoreWithProducer(…) infers the context type properly with a produce count: 0 }, on: { - inc: (ctx, ev: { by: number }, enq) => { + inc: (ctx, ev: { by: number }) => { ctx.count += ev.by; } } @@ -180,9 +181,9 @@ it('can be observed', () => { count: 0 }, { - inc: { - count: (ctx) => ctx.count + 1 - } + inc: (ctx) => ({ + count: ctx.count + 1 + }) } ); @@ -211,9 +212,9 @@ it('can be inspected', () => { count: 0 }, { - inc: { - count: (ctx) => ctx.count + 1 - } + inc: (ctx) => ({ + count: ctx.count + 1 + }) } ); diff --git a/packages/xstate-store/test/types.test.tsx b/packages/xstate-store/test/types.test.tsx index 2bbf40e24a..6cd59d3d35 100644 --- a/packages/xstate-store/test/types.test.tsx +++ b/packages/xstate-store/test/types.test.tsx @@ -10,11 +10,9 @@ describe('emitted', () => { }, context: {}, on: { - inc: { - count: (ctx, _: {}, enq) => { - enq.emit({ type: 'increased', upBy: 1 }); - return ctx; - } + inc: (ctx, _, enq) => { + enq.emit({ type: 'increased', upBy: 1 }); + return ctx; } } }); @@ -29,14 +27,12 @@ describe('emitted', () => { }, context: {}, on: { - inc: { - count: (ctx, _: {}, enq) => { - enq.emit({ - // @ts-expect-error - type: 'unknown' - }); - return ctx; - } + inc: (ctx, _, enq) => { + enq.emit({ + // @ts-expect-error + type: 'unknown' + }); + return ctx; } } }); @@ -51,15 +47,13 @@ describe('emitted', () => { }, context: {}, on: { - inc: { - count: (ctx, _: {}, enq) => { - enq.emit({ - type: 'increased', - // @ts-expect-error - upBy: 'bazinga' - }); - return ctx; - } + inc: (ctx, _, enq) => { + enq.emit({ + type: 'increased', + // @ts-expect-error + upBy: 'bazinga' + }); + return ctx; } } }); @@ -69,13 +63,11 @@ describe('emitted', () => { createStore({ context: {}, on: { - inc: { - count: (ctx, _: {}, enq) => { - enq.emit({ - type: 'unknown' - }); - return ctx; - } + inc: (ctx, _, enq) => { + enq.emit({ + type: 'unknown' + }); + return ctx; } } }); From a0b5dd2f92ffd45d32572dae943cc850f80b7297 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 18 Jan 2025 19:28:43 -0500 Subject: [PATCH 02/15] `createStore` only takes a single { context, on } argument now --- .changeset/five-walls-approve.md | 5 ++ packages/xstate-store/src/store.ts | 55 ++----------------- packages/xstate-store/test/UseSelector.vue | 8 +-- packages/xstate-store/test/react.test.tsx | 32 +++++------ packages/xstate-store/test/solid.test.tsx | 16 +++--- packages/xstate-store/test/store.test.ts | 62 +++++++++++----------- 6 files changed, 68 insertions(+), 110 deletions(-) create mode 100644 .changeset/five-walls-approve.md diff --git a/.changeset/five-walls-approve.md b/.changeset/five-walls-approve.md new file mode 100644 index 0000000000..83410651b0 --- /dev/null +++ b/.changeset/five-walls-approve.md @@ -0,0 +1,5 @@ +--- +'@xstate/store': major +--- + +`createStore` only takes a single { context, on } argument now diff --git a/packages/xstate-store/src/store.ts b/packages/xstate-store/src/store.ts index 89c13ab3d0..5c1c121009 100644 --- a/packages/xstate-store/src/store.ts +++ b/packages/xstate-store/src/store.ts @@ -242,8 +242,7 @@ export function createStore< TTypes extends { emitted?: EventObject } >({ context, - on, - types + on }: { context: TContext; on: { @@ -257,56 +256,8 @@ export function createStore< TContext, ExtractEventsFromPayloadMap, Cast ->; - -/** - * Creates a **store** that has its own internal state and can be sent events - * that update its internal state based on transitions. - * - * @example - * - * ```ts - * const store = createStore( - * // Initial context - * { count: 0 }, - * // Transitions - * { - * inc: (context, event: { by: number }) => { - * return { - * count: context.count + event.by - * }; - * } - * } - * ); - * - * store.subscribe((snapshot) => { - * console.log(snapshot); - * }); - * - * store.send({ type: 'inc', by: 5 }); - * // Logs { context: { count: 5 }, status: 'active', ... } - * ``` - */ -export function createStore< - TContext extends StoreContext, - TEventPayloadMap extends EventPayloadMap ->( - initialContext: TContext, - transitions: TransitionsFromEventPayloadMap< - TEventPayloadMap, - TContext, - EventObject - > -): Store, EventObject>; - -export function createStore(initialContextOrObject: any, transitions?: any) { - if (transitions === undefined) { - return createStoreCore( - initialContextOrObject.context, - initialContextOrObject.on - ); - } - return createStoreCore(initialContextOrObject, transitions); +> { + return createStoreCore(context, on); } /** diff --git a/packages/xstate-store/test/UseSelector.vue b/packages/xstate-store/test/UseSelector.vue index 9f4c09019f..d38af28c50 100644 --- a/packages/xstate-store/test/UseSelector.vue +++ b/packages/xstate-store/test/UseSelector.vue @@ -14,14 +14,14 @@ import { createStore } from '../src/index.ts'; export default defineComponent({ emits: ['rerender'], setup() { - const store = createStore( - { + const store = createStore({ + context: { count: 0 }, - { + on: { inc: (ctx) => ({ count: ctx.count + 1 }) } - ); + }); const count = useSelector(store, (state) => state.context.count); count satisfies Ref; diff --git a/packages/xstate-store/test/react.test.tsx b/packages/xstate-store/test/react.test.tsx index 889c025ef5..d626412803 100644 --- a/packages/xstate-store/test/react.test.tsx +++ b/packages/xstate-store/test/react.test.tsx @@ -9,17 +9,17 @@ import { import ReactDOM from 'react-dom'; it('useSelector should work', () => { - const store = createStore( - { + const store = createStore({ + context: { count: 0 }, - { + on: { inc: (ctx) => ({ ...ctx, count: ctx.count + 1 }) } - ); + }); const Counter = () => { const count = useSelector(store, (s) => s.context.count); @@ -48,11 +48,11 @@ it('useSelector should work', () => { }); it('useSelector can take in a custom comparator', () => { - const store = createStore( - { + const store = createStore({ + context: { items: [1, 2] }, - { + on: { same: (ctx) => ({ ...ctx, items: [1, 2] // different array, same items @@ -62,7 +62,7 @@ it('useSelector can take in a custom comparator', () => { items: [3, 4] }) } - ); + }); let renderCount = 0; const Items = () => { @@ -115,17 +115,17 @@ it('useSelector can take in a custom comparator', () => { }); it('can batch updates', () => { - const store = createStore( - { + const store = createStore({ + context: { count: 0 }, - { + on: { inc: (ctx) => ({ ...ctx, count: ctx.count + 1 }) } - ); + }); const Counter = () => { const count = useSelector(store, (s) => s.context.count); @@ -157,17 +157,17 @@ it('can batch updates', () => { }); it('useSelector (@xstate/react) should work', () => { - const store = createStore( - { + const store = createStore({ + context: { count: 0 }, - { + on: { inc: (ctx) => ({ ...ctx, count: ctx.count + 1 }) } - ); + }); const Counter = () => { const count = useXStateSelector(store, (s) => s.context.count); diff --git a/packages/xstate-store/test/solid.test.tsx b/packages/xstate-store/test/solid.test.tsx index fd94bd5a00..1d8698cfae 100644 --- a/packages/xstate-store/test/solid.test.tsx +++ b/packages/xstate-store/test/solid.test.tsx @@ -19,9 +19,9 @@ const useRenderTracker = (...accessors: Accessor[]) => { /** A commonly reused store for testing selector behaviours. */ const createCounterStore = () => - createStore( - { count: 0, other: 0 }, - { + createStore({ + context: { count: 0, other: 0 }, + on: { increment: (ctx) => ({ ...ctx, count: ctx.count + 1 @@ -31,7 +31,7 @@ const createCounterStore = () => other: ctx.other + 1 }) } - ); + }); describe('Solid.js integration', () => { describe('useSelector', () => { @@ -78,9 +78,9 @@ describe('Solid.js integration', () => { const INITIAL_ITEMS_STRING = INITIAL_ITEMS.join(','); const DIFFERENT_ITEMS_STRING = DIFFERENT_ITEMS.join(','); - const store = createStore( - { items: INITIAL_ITEMS }, - { + const store = createStore({ + context: { items: INITIAL_ITEMS }, + on: { same: (ctx) => ({ ...ctx, items: [...INITIAL_ITEMS] @@ -90,7 +90,7 @@ describe('Solid.js integration', () => { items: DIFFERENT_ITEMS }) } - ); + }); const ItemList: Component<{ itemStore: typeof store; diff --git a/packages/xstate-store/test/store.test.ts b/packages/xstate-store/test/store.test.ts index aa270547aa..2b286be72d 100644 --- a/packages/xstate-store/test/store.test.ts +++ b/packages/xstate-store/test/store.test.ts @@ -4,11 +4,14 @@ import { createBrowserInspector } from '@statelyai/inspect'; it('updates a store with an event without mutating original context', () => { const context = { count: 0 }; - const store = createStore(context, { - inc: (context, event: { by: number }) => { - return { - count: context.count + event.by - }; + const store = createStore({ + context, + on: { + inc: (context, event: { by: number }) => { + return { + count: context.count + event.by + }; + } } }); @@ -24,9 +27,9 @@ it('updates a store with an event without mutating original context', () => { }); it('can update context with a property assigner', () => { - const store = createStore( - { count: 0, greeting: 'hello' }, - { + const store = createStore({ + context: { count: 0, greeting: 'hello' }, + on: { inc: (ctx) => ({ ...ctx, count: ctx.count + 1 @@ -36,7 +39,7 @@ it('can update context with a property assigner', () => { greeting: 'hi' }) } - ); + }); store.send({ type: 'inc' @@ -50,14 +53,14 @@ it('can update context with a property assigner', () => { }); it('handles unknown events (does not do anything)', () => { - const store = createStore( - { count: 0 }, - { + const store = createStore({ + context: { count: 0 }, + on: { inc: (ctx) => ({ count: ctx.count + 1 }) } - ); + }); store.send({ // @ts-expect-error @@ -67,11 +70,11 @@ it('handles unknown events (does not do anything)', () => { }); it('updates state from sent events', () => { - const store = createStore( - { + const store = createStore({ + context: { count: 0 }, - { + on: { inc: (ctx, ev: { by: number }) => { return { count: ctx.count + ev.by @@ -88,7 +91,7 @@ it('updates state from sent events', () => { }; } } - ); + }); store.send({ type: 'inc', by: 9 }); store.send({ type: 'dec', by: 3 }); @@ -100,17 +103,16 @@ it('updates state from sent events', () => { }); it('createStoreWithProducer(…) works with an immer producer', () => { - const store = createStoreWithProducer( - produce, - { + const store = createStoreWithProducer(produce, { + context: { count: 0 }, - { + on: { inc: (ctx, ev: { by: number }) => { ctx.count += ev.by; } } - ); + }); store.send({ type: 'inc', by: 3 }); store.send({ @@ -176,16 +178,16 @@ it('createStoreWithProducer(…) infers the context type properly with a produce }); it('can be observed', () => { - const store = createStore( - { + const store = createStore({ + context: { count: 0 }, - { + on: { inc: (ctx) => ({ count: ctx.count + 1 }) } - ); + }); const counts: number[] = []; @@ -207,16 +209,16 @@ it('can be observed', () => { }); it('can be inspected', () => { - const store = createStore( - { + const store = createStore({ + context: { count: 0 }, - { + on: { inc: (ctx) => ({ count: ctx.count + 1 }) } - ); + }); const evs: any[] = []; From 4f3d224ae36b1ad696fc42b349d8afe51916f1f3 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 19 Jan 2025 12:49:52 -0500 Subject: [PATCH 03/15] createStoreWithProducer --- .changeset/spotty-moose-joke.md | 5 ++ packages/xstate-store/src/fromStore.ts | 35 ++++------- packages/xstate-store/src/store.ts | 62 ++++---------------- packages/xstate-store/src/types.ts | 28 ++------- packages/xstate-store/test/fromStore.test.ts | 25 +++++--- packages/xstate-store/test/store.test.ts | 9 ++- 6 files changed, 52 insertions(+), 112 deletions(-) create mode 100644 .changeset/spotty-moose-joke.md diff --git a/.changeset/spotty-moose-joke.md b/.changeset/spotty-moose-joke.md new file mode 100644 index 0000000000..1a706ce688 --- /dev/null +++ b/.changeset/spotty-moose-joke.md @@ -0,0 +1,5 @@ +--- +'@xstate/store': major +--- + +createStoreWithProducer now only accepts a single config object diff --git a/packages/xstate-store/src/fromStore.ts b/packages/xstate-store/src/fromStore.ts index 5469e4b9ea..70908aec08 100644 --- a/packages/xstate-store/src/fromStore.ts +++ b/packages/xstate-store/src/fromStore.ts @@ -7,8 +7,7 @@ import { StoreSnapshot, EventObject, ExtractEventsFromPayloadMap, - StoreAssigner, - StorePropertyAssigner + StoreAssigner } from './types'; type StoreLogic< @@ -66,17 +65,11 @@ export function fromStore< config: { context: ((input: TInput) => TContext) | TContext; on: { - [K in keyof TEventPayloadMap & string]: - | StoreAssigner< - NoInfer, - { type: K } & TEventPayloadMap[K], - Cast - > - | StorePropertyAssigner< - NoInfer, - { type: K } & TEventPayloadMap[K], - Cast - >; + [K in keyof TEventPayloadMap & string]: StoreAssigner< + NoInfer, + { type: K } & TEventPayloadMap[K], + Cast + >; }; } & { types?: TTypes } ): StoreLogic< @@ -97,17 +90,11 @@ export function fromStore< | ({ context: ((input: TInput) => TContext) | TContext; on: { - [K in keyof TEventPayloadMap & string]: - | StoreAssigner< - NoInfer, - { type: K } & TEventPayloadMap[K], - Cast - > - | StorePropertyAssigner< - NoInfer, - { type: K } & TEventPayloadMap[K], - Cast - >; + [K in keyof TEventPayloadMap & string]: StoreAssigner< + NoInfer, + { type: K } & TEventPayloadMap[K], + Cast + >; }; } & { types?: TTypes }), transitions?: TransitionsFromEventPayloadMap< diff --git a/packages/xstate-store/src/store.ts b/packages/xstate-store/src/store.ts index 5c1c121009..33bd54bc8e 100644 --- a/packages/xstate-store/src/store.ts +++ b/packages/xstate-store/src/store.ts @@ -11,6 +11,7 @@ import { StoreAssigner, StoreContext, StoreInspectionEvent, + StoreProducerAssigner, StoreSnapshot } from './types'; @@ -60,9 +61,9 @@ function createStoreCore< TEmitted >; }, - updater?: ( + producer?: ( context: NoInfer, - recipe: (context: NoInfer) => NoInfer + recipe: (context: NoInfer) => void ) => NoInfer ): Store, TEmitted> { type StoreEvent = ExtractEventsFromPayloadMap; @@ -87,7 +88,7 @@ function createStoreCore< } }; - const transition = createStoreTransition(transitions, updater); + const transition = createStoreTransition(transitions, producer); function receive(event: StoreEvent) { let emitted: TEmitted[]; @@ -308,49 +309,8 @@ export function createStoreWithProducer< ) => void; }; } -): Store, TEmitted>; -export function createStoreWithProducer< - TContext extends StoreContext, - TEventPayloadMap extends EventPayloadMap, - TEmitted extends EventObject = EventObject ->( - producer: NoInfer< - (context: TContext, recipe: (context: TContext) => void) => TContext - >, - initialContext: TContext, - transitions: { - [K in keyof TEventPayloadMap & string]: ( - context: NoInfer, - event: { type: K } & TEventPayloadMap[K], - enqueue: EnqueueObject - ) => void; - } -): Store, TEmitted>; - -export function createStoreWithProducer< - TContext extends StoreContext, - TEventPayloadMap extends EventPayloadMap, - TEmitted extends EventObject = EventObject ->( - producer: ( - context: TContext, - recipe: (context: TContext) => void - ) => TContext, - initialContextOrConfig: any, - transitions?: any ): Store, TEmitted> { - if ( - typeof initialContextOrConfig === 'object' && - 'context' in initialContextOrConfig && - 'on' in initialContextOrConfig - ) { - return createStoreCore( - initialContextOrConfig.context, - initialContextOrConfig.on, - producer - ); - } - return createStoreCore(initialContextOrConfig, transitions, producer); + return createStoreCore(config.context, config.on, producer); } declare global { @@ -364,7 +324,7 @@ declare global { * snapshot and an event and returns a new snapshot. * * @param transitions - * @param updater + * @param producer * @returns */ export function createStoreTransition< @@ -379,9 +339,9 @@ export function createStoreTransition< TEmitted >; }, - updater?: ( + producer?: ( context: TContext, - recipe: (context: TContext) => TContext + recipe: (context: TContext) => void ) => TContext ) { return ( @@ -404,9 +364,9 @@ export function createStoreTransition< } if (typeof assigner === 'function') { - currentContext = updater - ? updater(currentContext, (draftContext) => - (assigner as StoreAssigner)?.( + currentContext = producer + ? producer(currentContext, (draftContext) => + (assigner as StoreProducerAssigner)( draftContext, event, enqueue diff --git a/packages/xstate-store/src/types.ts b/packages/xstate-store/src/types.ts index 3dae424ae7..99a0a764cc 100644 --- a/packages/xstate-store/src/types.ts +++ b/packages/xstate-store/src/types.ts @@ -18,31 +18,13 @@ export type StoreAssigner< context: TContext, event: TEvent, enq: EnqueueObject -) => TContext; -export type StoreCompleteAssigner< - TContext, - TEvent extends EventObject, - TEmitted extends EventObject -> = (ctx: TContext, ev: TEvent, enq: EnqueueObject) => TContext; -export type StorePartialAssigner< - TContext, - TEvent extends EventObject, - K extends keyof TContext, - TEmitted extends EventObject -> = ( - ctx: TContext, - ev: TEvent, - enq: EnqueueObject -) => Partial[K]; -export type StorePropertyAssigner< - TContext, +) => TContext | void; + +export type StoreProducerAssigner< + TContext extends StoreContext, TEvent extends EventObject, TEmitted extends EventObject -> = { - [K in keyof TContext]?: - | TContext[K] - | StorePartialAssigner; -}; +> = (context: TContext, event: TEvent, enq: EnqueueObject) => void; export type Snapshot = | { diff --git a/packages/xstate-store/test/fromStore.test.ts b/packages/xstate-store/test/fromStore.test.ts index d1d7e2349e..1cacf94f8c 100644 --- a/packages/xstate-store/test/fromStore.test.ts +++ b/packages/xstate-store/test/fromStore.test.ts @@ -25,10 +25,16 @@ describe('fromStore', () => { const storeLogic = fromStore({ context: (count: number) => ({ count }), on: { - inc: { - count: (ctx, ev: { by: number }) => { - return ctx.count + ev.by; - } + // inc: { + // count: (ctx, ev: { by: number }) => { + // return ctx.count + ev.by; + // } + // } + inc: (ctx, ev: { by: number }) => { + return { + ...ctx, + count: ctx.count + ev.by + }; } } }); @@ -53,11 +59,12 @@ describe('fromStore', () => { }, context: (count: number) => ({ count }), on: { - inc: { - count: (ctx, ev: { by: number }, enq) => { - enq.emit({ type: 'increased', upBy: ev.by }); - return ctx.count + ev.by; - } + inc: (ctx, ev: { by: number }, enq) => { + enq.emit({ type: 'increased', upBy: ev.by }); + return { + ...ctx, + count: ctx.count + ev.by + }; } } }); diff --git a/packages/xstate-store/test/store.test.ts b/packages/xstate-store/test/store.test.ts index 2b286be72d..b1d1a1d9c5 100644 --- a/packages/xstate-store/test/store.test.ts +++ b/packages/xstate-store/test/store.test.ts @@ -147,17 +147,16 @@ it('createStoreWithProducer(…) works with an immer producer (object API)', () }); it('createStoreWithProducer(…) infers the context type properly with a producer', () => { - const store = createStoreWithProducer( - produce, - { + const store = createStoreWithProducer(produce, { + context: { count: 0 }, - { + on: { inc: (ctx, ev: { by: number }) => { ctx.count += ev.by; } } - ); + }); store.getSnapshot().context satisfies { count: number }; }); From 4d2ddf0773c512e1799f9fff2613ceea05689829 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 21 Jan 2025 21:29:29 -0500 Subject: [PATCH 04/15] Enhance store functionality by introducing effects in event handling - Added `StoreEffect` type to support both emitted events and side effects. - Updated `createStoreTransition` to return effects instead of emitted events. - Modified `receive` function to handle effects, executing functions or emitting events accordingly. - Added a test case to verify that effects can be enqueued and executed after state updates. This change improves the flexibility of the store's event handling mechanism. --- .changeset/great-candles-rule.md | 5 ++++ packages/xstate-store/src/store.ts | 28 ++++++++++++------- packages/xstate-store/src/types.ts | 3 +++ packages/xstate-store/test/store.test.ts | 34 ++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 .changeset/great-candles-rule.md diff --git a/.changeset/great-candles-rule.md b/.changeset/great-candles-rule.md new file mode 100644 index 0000000000..04b6768cb9 --- /dev/null +++ b/.changeset/great-candles-rule.md @@ -0,0 +1,5 @@ +--- +'@xstate/store': major +--- + +Add `enq.effect(…)` diff --git a/packages/xstate-store/src/store.ts b/packages/xstate-store/src/store.ts index 33bd54bc8e..e909a538c7 100644 --- a/packages/xstate-store/src/store.ts +++ b/packages/xstate-store/src/store.ts @@ -10,6 +10,7 @@ import { Store, StoreAssigner, StoreContext, + StoreEffect, StoreInspectionEvent, StoreProducerAssigner, StoreSnapshot @@ -91,8 +92,8 @@ function createStoreCore< const transition = createStoreTransition(transitions, producer); function receive(event: StoreEvent) { - let emitted: TEmitted[]; - [currentSnapshot, emitted] = transition(currentSnapshot, event); + let effects: StoreEffect[]; + [currentSnapshot, effects] = transition(currentSnapshot, event); inspectionObservers.get(store)?.forEach((observer) => { observer.next?.({ @@ -106,7 +107,13 @@ function createStoreCore< observers?.forEach((o) => o.next?.(currentSnapshot)); - emitted.forEach(emit); + for (const effect of effects) { + if (typeof effect === 'function') { + effect(); + } else { + emit(effect); + } + } } const store: Store = { @@ -347,20 +354,23 @@ export function createStoreTransition< return ( snapshot: StoreSnapshot, event: ExtractEventsFromPayloadMap - ): [StoreSnapshot, TEmitted[]] => { + ): [StoreSnapshot, StoreEffect[]] => { type StoreEvent = ExtractEventsFromPayloadMap; let currentContext = snapshot.context; const assigner = transitions?.[event.type as StoreEvent['type']]; - const emitted: TEmitted[] = []; + const effects: StoreEffect[] = []; - const enqueue = { + const enqueue: EnqueueObject = { emit: (ev: TEmitted) => { - emitted.push(ev); + effects.push(ev); + }, + effect: (fn) => { + effects.push(fn); } }; if (!assigner) { - return [snapshot, emitted]; + return [snapshot, effects]; } if (typeof assigner === 'function') { @@ -399,7 +409,7 @@ export function createStoreTransition< currentContext = Object.assign({}, currentContext, partialUpdate); } - return [{ ...snapshot, context: currentContext }, emitted]; + return [{ ...snapshot, context: currentContext }, effects]; }; } diff --git a/packages/xstate-store/src/types.ts b/packages/xstate-store/src/types.ts index 99a0a764cc..e2bc23cef3 100644 --- a/packages/xstate-store/src/types.ts +++ b/packages/xstate-store/src/types.ts @@ -8,8 +8,11 @@ export type Recipe = (state: T) => TReturn; export type EnqueueObject = { emit: (ev: TEmitted) => void; + effect: (fn: () => void) => void; }; +export type StoreEffect = (() => void) | TEmitted; + export type StoreAssigner< TContext extends StoreContext, TEvent extends EventObject, diff --git a/packages/xstate-store/test/store.test.ts b/packages/xstate-store/test/store.test.ts index b1d1a1d9c5..d8e93b8fc8 100644 --- a/packages/xstate-store/test/store.test.ts +++ b/packages/xstate-store/test/store.test.ts @@ -352,3 +352,37 @@ it('emitted events occur after the snapshot is updated', () => { store.send({ type: 'inc' }); }); + +it('effects can be enqueued', async () => { + const store = createStore({ + context: { + count: 0 + }, + on: { + inc: (ctx, _, enq) => { + enq.effect(() => { + setTimeout(() => { + store.send({ type: 'dec' }); + }, 5); + }); + + return { + ...ctx, + count: ctx.count + 1 + }; + }, + dec: (ctx) => ({ + ...ctx, + count: ctx.count - 1 + }) + } + }); + + store.send({ type: 'inc' }); + + expect(store.getSnapshot().context.count).toEqual(1); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(store.getSnapshot().context.count).toEqual(0); +}); From d55e9635e3a006c29615b5f2601e9cdfcbe267e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 22 Jan 2025 11:56:32 +0100 Subject: [PATCH 05/15] =?UTF-8?q?use=20overload=20trick=20=F0=9F=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/xstate-store/src/store.ts | 73 ++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/packages/xstate-store/src/store.ts b/packages/xstate-store/src/store.ts index e909a538c7..6bdd8eed38 100644 --- a/packages/xstate-store/src/store.ts +++ b/packages/xstate-store/src/store.ts @@ -215,6 +215,33 @@ export type TransitionsFromEventPayloadMap< >; }; +type CreateStoreParameterTypes< + TContext extends StoreContext, + TEventPayloadMap extends EventPayloadMap, + TTypes extends { emitted?: EventObject } +> = [ + definition: { + context: TContext; + on: { + [K in keyof TEventPayloadMap & string]: StoreAssigner< + NoInfer, + { type: K } & TEventPayloadMap[K], + Cast + >; + }; + } & { types?: TTypes } +]; + +type CreateStoreReturnType< + TContext extends StoreContext, + TEventPayloadMap extends EventPayloadMap, + TTypes extends { emitted?: EventObject } +> = Store< + TContext, + ExtractEventsFromPayloadMap, + Cast +>; + /** * Creates a **store** that has its own internal state and can be sent events * that update its internal state based on transitions. @@ -244,30 +271,40 @@ export type TransitionsFromEventPayloadMap< * // Logs { context: { count: 5 }, status: 'active', ... } * ``` */ -export function createStore< +function _createStore< TContext extends StoreContext, TEventPayloadMap extends EventPayloadMap, TTypes extends { emitted?: EventObject } ->({ - context, - on -}: { - context: TContext; - on: { - [K in keyof TEventPayloadMap & string]: StoreAssigner< - NoInfer, - { type: K } & TEventPayloadMap[K], - Cast - >; - }; -} & { types?: TTypes }): Store< - TContext, - ExtractEventsFromPayloadMap, - Cast -> { +>( + ...[{ context, on }]: CreateStoreParameterTypes< + TContext, + TEventPayloadMap, + TTypes + > +): CreateStoreReturnType { return createStoreCore(context, on); } +export const createStore: { + // those overloads are exactly the same, we only duplicate them so TypeScript can: + // 1. assign contextual parameter types during inference attempt for the first overload when the source object is still context-sensitive and often non-inferrable + // 2. infer correctly during inference attempt for the second overload when the parameter types are already "known" + < + TContext extends StoreContext, + TEventPayloadMap extends EventPayloadMap, + TTypes extends { emitted?: EventObject } + >( + ...args: CreateStoreParameterTypes + ): CreateStoreReturnType; + < + TContext extends StoreContext, + TEventPayloadMap extends EventPayloadMap, + TTypes extends { emitted?: EventObject } + >( + ...args: CreateStoreParameterTypes + ): CreateStoreReturnType; +} = _createStore; + /** * Creates a `Store` with a provided producer (such as Immer's `producer(…)` A * store has its own internal state and can receive events. From 73c9489cba15a5e853a86ce37ebb418564d79be6 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 22 Jan 2025 21:14:06 -0500 Subject: [PATCH 06/15] Fix fromStore --- packages/xstate-store/src/fromStore.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/xstate-store/src/fromStore.ts b/packages/xstate-store/src/fromStore.ts index 70908aec08..179605a32d 100644 --- a/packages/xstate-store/src/fromStore.ts +++ b/packages/xstate-store/src/fromStore.ts @@ -129,9 +129,15 @@ export function fromStore< const transition = createStoreTransition(transitionsObj); return { transition: (snapshot, event, actorScope) => { - const [nextSnapshot, emittedEvents] = transition(snapshot, event); + const [nextSnapshot, effects] = transition(snapshot, event); - emittedEvents.forEach(actorScope.emit); + for (const effect of effects) { + if (typeof effect === 'function') { + effect(); + } else { + actorScope.emit(effect); + } + } return nextSnapshot; }, From 3d2cded75c86ac0cc0d3e5296c6f98ee27818a45 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 25 Jan 2025 08:41:33 -0500 Subject: [PATCH 07/15] Update changesets --- .changeset/five-walls-approve.md | 24 +++++++++++++++++++++++- .changeset/great-candles-rule.md | 21 ++++++++++++++++++++- .changeset/spotty-moose-joke.md | 25 ++++++++++++++++++++++++- .changeset/thick-paws-invite.md | 16 +++++++++++++++- 4 files changed, 82 insertions(+), 4 deletions(-) diff --git a/.changeset/five-walls-approve.md b/.changeset/five-walls-approve.md index 83410651b0..9193469d5c 100644 --- a/.changeset/five-walls-approve.md +++ b/.changeset/five-walls-approve.md @@ -2,4 +2,26 @@ '@xstate/store': major --- -`createStore` only takes a single { context, on } argument now +The `createStore` function now only accepts a single configuration object argument. This is a breaking change that simplifies the API and aligns with the configuration pattern used throughout XState. + +```ts +// Before +// createStore( +// { +// count: 0 +// }, +// { +// increment: (context) => ({ count: context.count + 1 }) +// } +// ); + +// After +createStore({ + context: { + count: 0 + }, + on: { + increment: (context) => ({ count: context.count + 1 }) + } +}); +``` diff --git a/.changeset/great-candles-rule.md b/.changeset/great-candles-rule.md index 04b6768cb9..c3233a0b2d 100644 --- a/.changeset/great-candles-rule.md +++ b/.changeset/great-candles-rule.md @@ -2,4 +2,23 @@ '@xstate/store': major --- -Add `enq.effect(…)` +You can now enqueue effects in state transitions. + +```ts +const store = createStore({ + context: { + count: 0 + }, + on: { + incrementDelayed: (context, event, enq) => { + enq.effect(async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + store.send({ type: 'increment' }); + }); + + return context; + }, + increment: (context) => ({ count: context.count + 1 }) + } +}); +``` diff --git a/.changeset/spotty-moose-joke.md b/.changeset/spotty-moose-joke.md index 1a706ce688..cf76b09472 100644 --- a/.changeset/spotty-moose-joke.md +++ b/.changeset/spotty-moose-joke.md @@ -2,4 +2,27 @@ '@xstate/store': major --- -createStoreWithProducer now only accepts a single config object +The `createStoreWithProducer(…)` function now only accepts two arguments: a `producer` and a config (`{ context, on }`) object. + +```ts +// Before +// createStoreWithProducer( +// producer, +// { +// count: 0 +// }, +// { +// increment: (context) => ({ count: context.count + 1 }) +// } +// ); + +// After +createStoreWithProducer(producer, { + context: { + count: 0 + }, + on: { + increment: (context) => ({ count: context.count + 1 }) + } +}); +``` diff --git a/.changeset/thick-paws-invite.md b/.changeset/thick-paws-invite.md index 73f1274018..ea9cd7668e 100644 --- a/.changeset/thick-paws-invite.md +++ b/.changeset/thick-paws-invite.md @@ -2,4 +2,18 @@ '@xstate/store': major --- -Only complete assigners can now be used +Only complete assigner functions that replace the `context` fully are supported. This is a breaking change that simplifies the API and provides more type safety. + +```diff +const store = createStore({ + context: { + items: [], + count: 0 + }, + on: { +- increment: { count: (context) => context.count + 1 } +- increment: (context) => ({ count: context.count + 1 }) ++ increment: (context) => ({ ...context, count: context.count + 1 }) + } +}) +``` From cfe28e66597470c5100e918d1f24cc8ca00751cc Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 25 Jan 2025 09:16:00 -0500 Subject: [PATCH 08/15] Add support for type parameters --- packages/xstate-store/src/store.ts | 19 +++++++----- packages/xstate-store/test/types.test.tsx | 38 +++++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/packages/xstate-store/src/store.ts b/packages/xstate-store/src/store.ts index 6bdd8eed38..65e6be05fc 100644 --- a/packages/xstate-store/src/store.ts +++ b/packages/xstate-store/src/store.ts @@ -289,13 +289,18 @@ export const createStore: { // those overloads are exactly the same, we only duplicate them so TypeScript can: // 1. assign contextual parameter types during inference attempt for the first overload when the source object is still context-sensitive and often non-inferrable // 2. infer correctly during inference attempt for the second overload when the parameter types are already "known" - < - TContext extends StoreContext, - TEventPayloadMap extends EventPayloadMap, - TTypes extends { emitted?: EventObject } - >( - ...args: CreateStoreParameterTypes - ): CreateStoreReturnType; + (definition: { + context: TContext; + on: { + [K in TEvent['type']]?: StoreAssigner< + NoInfer, + { type: K } & TEvent, + TEvent + >; + }; + types?: never; + }): Store; + < TContext extends StoreContext, TEventPayloadMap extends EventPayloadMap, diff --git a/packages/xstate-store/test/types.test.tsx b/packages/xstate-store/test/types.test.tsx index 6cd59d3d35..d448fc83a2 100644 --- a/packages/xstate-store/test/types.test.tsx +++ b/packages/xstate-store/test/types.test.tsx @@ -107,3 +107,41 @@ describe('emitted', () => { ); }); }); + +describe('type parameters', () => { + it('type parameters can be provided', () => { + createStore< + { count: number }, + | { type: 'increment'; by: number } + | { type: 'decrement' } + | { type: 'other' } + >({ + context: { + count: 0 + }, + on: { + increment: (ctx, ev) => { + ev.by satisfies number; + + // @ts-expect-error + ev.by satisfies string; + + return { ...ctx, count: ctx.count + ev.by }; + }, + + // @ts-expect-error + whatever: (ctx) => ({ ...ctx, count: 1 }), + + decrement: (ctx) => { + // @ts-expect-error + ctx.whatever; + }, + + // @ts-expect-error + other: () => ({ + count: 'whatever' + }) + } + }); + }); +}); From c0efebf87aade299c19feec048c693723e82dc77 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 26 Jan 2025 10:59:03 -0500 Subject: [PATCH 09/15] Revert "Add support for type parameters" This reverts commit cfe28e66597470c5100e918d1f24cc8ca00751cc. --- packages/xstate-store/src/store.ts | 19 +++++------- packages/xstate-store/test/types.test.tsx | 38 ----------------------- 2 files changed, 7 insertions(+), 50 deletions(-) diff --git a/packages/xstate-store/src/store.ts b/packages/xstate-store/src/store.ts index 65e6be05fc..6bdd8eed38 100644 --- a/packages/xstate-store/src/store.ts +++ b/packages/xstate-store/src/store.ts @@ -289,18 +289,13 @@ export const createStore: { // those overloads are exactly the same, we only duplicate them so TypeScript can: // 1. assign contextual parameter types during inference attempt for the first overload when the source object is still context-sensitive and often non-inferrable // 2. infer correctly during inference attempt for the second overload when the parameter types are already "known" - (definition: { - context: TContext; - on: { - [K in TEvent['type']]?: StoreAssigner< - NoInfer, - { type: K } & TEvent, - TEvent - >; - }; - types?: never; - }): Store; - + < + TContext extends StoreContext, + TEventPayloadMap extends EventPayloadMap, + TTypes extends { emitted?: EventObject } + >( + ...args: CreateStoreParameterTypes + ): CreateStoreReturnType; < TContext extends StoreContext, TEventPayloadMap extends EventPayloadMap, diff --git a/packages/xstate-store/test/types.test.tsx b/packages/xstate-store/test/types.test.tsx index d448fc83a2..6cd59d3d35 100644 --- a/packages/xstate-store/test/types.test.tsx +++ b/packages/xstate-store/test/types.test.tsx @@ -107,41 +107,3 @@ describe('emitted', () => { ); }); }); - -describe('type parameters', () => { - it('type parameters can be provided', () => { - createStore< - { count: number }, - | { type: 'increment'; by: number } - | { type: 'decrement' } - | { type: 'other' } - >({ - context: { - count: 0 - }, - on: { - increment: (ctx, ev) => { - ev.by satisfies number; - - // @ts-expect-error - ev.by satisfies string; - - return { ...ctx, count: ctx.count + ev.by }; - }, - - // @ts-expect-error - whatever: (ctx) => ({ ...ctx, count: 1 }), - - decrement: (ctx) => { - // @ts-expect-error - ctx.whatever; - }, - - // @ts-expect-error - other: () => ({ - count: 'whatever' - }) - } - }); - }); -}); From 8528460dc9d961a6c1351bfba145a08b935b22c7 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 26 Jan 2025 13:46:41 -0500 Subject: [PATCH 10/15] Refactor store type parameters to improve type safety and flexibility - Updated `createStore` and `createStoreWithProducer` to use more explicit type parameters - Replaced `types: { emitted }` with separate type parameters for context, event payloads, and emitted events - Removed `Cast` import and simplified type definitions - Updated test cases to use new type parameter approach - Added `EventMap` type to support event type mapping --- .changeset/big-schools-hang.md | 37 ++++++++++++ packages/xstate-store/src/store.ts | 39 +++++++------ packages/xstate-store/src/types.ts | 4 ++ packages/xstate-store/test/store.test.ts | 31 +++++----- packages/xstate-store/test/types.test.tsx | 70 ++++++++++++++--------- 5 files changed, 120 insertions(+), 61 deletions(-) create mode 100644 .changeset/big-schools-hang.md diff --git a/.changeset/big-schools-hang.md b/.changeset/big-schools-hang.md new file mode 100644 index 0000000000..900309da76 --- /dev/null +++ b/.changeset/big-schools-hang.md @@ -0,0 +1,37 @@ +--- +'@xstate/store': major +--- + +Type parameters should now be explicitly provided to `createStore` and `createStoreWithProducer`. For sent and emitted events, the an `EventPayloadMap` should be provided, which is a map of event types to their payloads. + +```ts +createStore< + // Context + { + count: number; + }, + // Sent events + { + inc: { + by: number; + }; + }, + // Emitted events + { + increased: { + upBy: number; + }; + } +>({ + context: { count: 0 }, + on: { + inc: (ctx, event, enq) => { + enq.emit({ type: 'increased', upBy: event.by }); + return { + ...ctx, + count: ctx.count + event.by + }; + } + } +}); +``` diff --git a/packages/xstate-store/src/store.ts b/packages/xstate-store/src/store.ts index 6bdd8eed38..107602efa8 100644 --- a/packages/xstate-store/src/store.ts +++ b/packages/xstate-store/src/store.ts @@ -1,5 +1,4 @@ import { - Cast, EnqueueObject, EventObject, EventPayloadMap, @@ -218,7 +217,7 @@ export type TransitionsFromEventPayloadMap< type CreateStoreParameterTypes< TContext extends StoreContext, TEventPayloadMap extends EventPayloadMap, - TTypes extends { emitted?: EventObject } + TEmitted extends EventPayloadMap > = [ definition: { context: TContext; @@ -226,20 +225,20 @@ type CreateStoreParameterTypes< [K in keyof TEventPayloadMap & string]: StoreAssigner< NoInfer, { type: K } & TEventPayloadMap[K], - Cast + ExtractEventsFromPayloadMap >; }; - } & { types?: TTypes } + } ]; type CreateStoreReturnType< TContext extends StoreContext, TEventPayloadMap extends EventPayloadMap, - TTypes extends { emitted?: EventObject } + TEmitted extends EventPayloadMap > = Store< TContext, ExtractEventsFromPayloadMap, - Cast + ExtractEventsFromPayloadMap >; /** @@ -274,14 +273,14 @@ type CreateStoreReturnType< function _createStore< TContext extends StoreContext, TEventPayloadMap extends EventPayloadMap, - TTypes extends { emitted?: EventObject } + TEmitted extends EventPayloadMap >( ...[{ context, on }]: CreateStoreParameterTypes< TContext, TEventPayloadMap, - TTypes + TEmitted > -): CreateStoreReturnType { +): CreateStoreReturnType { return createStoreCore(context, on); } @@ -292,17 +291,17 @@ export const createStore: { < TContext extends StoreContext, TEventPayloadMap extends EventPayloadMap, - TTypes extends { emitted?: EventObject } + TEmitted extends EventPayloadMap >( - ...args: CreateStoreParameterTypes - ): CreateStoreReturnType; + ...args: CreateStoreParameterTypes + ): CreateStoreReturnType; < TContext extends StoreContext, TEventPayloadMap extends EventPayloadMap, - TTypes extends { emitted?: EventObject } + TEmitted extends EventObject >( - ...args: CreateStoreParameterTypes - ): CreateStoreReturnType; + ...args: CreateStoreParameterTypes + ): CreateStoreReturnType; } = _createStore; /** @@ -338,7 +337,7 @@ export const createStore: { export function createStoreWithProducer< TContext extends StoreContext, TEventPayloadMap extends EventPayloadMap, - TEmitted extends EventObject = EventObject + TEmittedPayloadMap extends EventPayloadMap >( producer: NoInfer< (context: TContext, recipe: (context: TContext) => void) => TContext @@ -349,11 +348,15 @@ export function createStoreWithProducer< [K in keyof TEventPayloadMap & string]: ( context: NoInfer, event: { type: K } & TEventPayloadMap[K], - enqueue: EnqueueObject + enqueue: EnqueueObject> ) => void; }; } -): Store, TEmitted> { +): Store< + TContext, + ExtractEventsFromPayloadMap, + ExtractEventsFromPayloadMap +> { return createStoreCore(config.context, config.on, producer); } diff --git a/packages/xstate-store/src/types.ts b/packages/xstate-store/src/types.ts index e2bc23cef3..d51ff976a9 100644 --- a/packages/xstate-store/src/types.ts +++ b/packages/xstate-store/src/types.ts @@ -285,3 +285,7 @@ export type ActorRefLike = { export type Prop = K extends keyof T ? T[K] : never; export type Cast = A extends B ? A : B; + +export type EventMap = { + [K in TEvent['type']]: TEvent & { type: K }; +}; diff --git a/packages/xstate-store/test/store.test.ts b/packages/xstate-store/test/store.test.ts index d8e93b8fc8..d6b821172f 100644 --- a/packages/xstate-store/test/store.test.ts +++ b/packages/xstate-store/test/store.test.ts @@ -258,12 +258,11 @@ it('inspection with @statelyai/inspect typechecks correctly', () => { }); it('emitted events can be subscribed to', () => { - const store = createStore({ - types: { - emitted: {} as - | { type: 'increased'; upBy: number } - | { type: 'decreased'; downBy: number } - }, + const store = createStore< + { count: number }, + { inc: {} }, + { increased: { upBy: number } } + >({ context: { count: 0 }, @@ -289,12 +288,11 @@ it('emitted events can be subscribed to', () => { }); it('emitted events can be unsubscribed to', () => { - const store = createStore({ - types: { - emitted: {} as - | { type: 'increased'; upBy: number } - | { type: 'decreased'; downBy: number } - }, + const store = createStore< + { count: number }, + { inc: {} }, + { increased: { upBy: number } } + >({ context: { count: 0 }, @@ -323,10 +321,11 @@ it('emitted events can be unsubscribed to', () => { }); it('emitted events occur after the snapshot is updated', () => { - const store = createStore({ - types: { - emitted: {} as { type: 'increased'; upBy: number } - }, + const store = createStore< + { count: number }, + { inc: {} }, + { increased: { upBy: number } } + >({ context: { count: 0 }, diff --git a/packages/xstate-store/test/types.test.tsx b/packages/xstate-store/test/types.test.tsx index 6cd59d3d35..877b4babf7 100644 --- a/packages/xstate-store/test/types.test.tsx +++ b/packages/xstate-store/test/types.test.tsx @@ -2,12 +2,16 @@ import { createStore } from '../src/index'; describe('emitted', () => { it('can emit a known event', () => { - createStore({ - types: { - emitted: {} as - | { type: 'increased'; upBy: number } - | { type: 'decreased'; downBy: number } + createStore< + {}, + { + inc: { upBy: number }; }, + { + increased: { upBy: number }; + decreased: { downBy: number }; + } + >({ context: {}, on: { inc: (ctx, _, enq) => { @@ -19,12 +23,16 @@ describe('emitted', () => { }); it("can't emit an unknown event", () => { - createStore({ - types: { - emitted: {} as - | { type: 'increased'; upBy: number } - | { type: 'decreased'; downBy: number } + createStore< + {}, + { + inc: { upBy: number }; }, + { + increased: { upBy: number }; + decreased: { downBy: number }; + } + >({ context: {}, on: { inc: (ctx, _, enq) => { @@ -39,12 +47,16 @@ describe('emitted', () => { }); it("can't emit a known event with wrong payload", () => { - createStore({ - types: { - emitted: {} as - | { type: 'increased'; upBy: number } - | { type: 'decreased'; downBy: number } + createStore< + {}, + { + inc: { upBy: number }; }, + { + increased: { upBy: number }; + decreased: { downBy: number }; + } + >({ context: {}, on: { inc: (ctx, _, enq) => { @@ -74,12 +86,14 @@ describe('emitted', () => { }); it('can subscribe to a known event', () => { - const store = createStore({ - types: { - emitted: {} as - | { type: 'increased'; upBy: number } - | { type: 'decreased'; downBy: number } - }, + const store = createStore< + {}, + {}, + { + increased: { upBy: number }; + decreased: { downBy: number }; + } + >({ context: {}, on: {} }); @@ -90,12 +104,14 @@ describe('emitted', () => { }); it("can can't subscribe to a unknown event", () => { - const store = createStore({ - types: { - emitted: {} as - | { type: 'increased'; upBy: number } - | { type: 'decreased'; downBy: number } - }, + const store = createStore< + {}, + {}, + { + increased: { upBy: number }; + decreased: { downBy: number }; + } + >({ context: {}, on: {} }); From 008badf268ee4610b8ba0a0a1d419132cd331e4f Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 26 Jan 2025 14:04:30 -0500 Subject: [PATCH 11/15] Add trigger + test + changeset --- .changeset/quick-bears-swim.md | 24 +++++++++ packages/xstate-store/src/store.ts | 17 +++++- packages/xstate-store/src/types.ts | 23 +++++++++ packages/xstate-store/test/store.test.ts | 66 +++++++++++++++++++++++- 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 .changeset/quick-bears-swim.md diff --git a/.changeset/quick-bears-swim.md b/.changeset/quick-bears-swim.md new file mode 100644 index 0000000000..830fc10e16 --- /dev/null +++ b/.changeset/quick-bears-swim.md @@ -0,0 +1,24 @@ +--- +'@xstate/store': minor +--- + +Added `store.trigger` API for sending events with a fluent interface: + +```ts +const store = createStore({ + context: { count: 0 }, + on: { + increment: (ctx, event: { by: number }) => ({ + count: ctx.count + event.by + }) + } +}); + +// Instead of manually constructing event objects: +store.send({ type: 'increment', by: 5 }); + +// You can now use the fluent trigger API: +store.trigger.increment({ by: 5 }); +``` + +The `trigger` API provides full type safety for event names and payloads, making it easier and safer to send events to the store. diff --git a/packages/xstate-store/src/store.ts b/packages/xstate-store/src/store.ts index 107602efa8..47fc8518d8 100644 --- a/packages/xstate-store/src/store.ts +++ b/packages/xstate-store/src/store.ts @@ -194,9 +194,24 @@ function createStoreCore< return inspectionObservers.get(store)?.delete(observer); } }; - } + }, + trigger: {} as any }; + (store as any).trigger = new Proxy( + {} as Store['trigger'], + { + get: (_, eventType: string) => { + return (payload: any) => { + store.send({ + type: eventType, + ...payload + }); + }; + } + } + ); + return store; } diff --git a/packages/xstate-store/src/types.ts b/packages/xstate-store/src/types.ts index d51ff976a9..bd3e6d7721 100644 --- a/packages/xstate-store/src/types.ts +++ b/packages/xstate-store/src/types.ts @@ -90,8 +90,31 @@ export interface Store< ev: Compute ) => void ) => Subscription; + /** + * A proxy object that allows you to send events to the store without manually + * constructing event objects. + * + * @example + * + * ```ts + * // Instead of: + * store.send({ type: 'increment', by: 1 }); + * + * // You can trigger the event: + * store.trigger.increment({ by: 1 }); + * ``` + */ + trigger: { + [K in TEvent['type'] & string]: IsEmptyObject< + Omit<{ type: K } & TEvent, 'type'> + > extends true + ? () => Omit<{ type: K } & TEvent, 'type'> + : (eventPayload: Omit<{ type: K } & TEvent, 'type'>) => void; + }; } +export type IsEmptyObject = T extends Record ? true : false; + export type AnyStore = Store; export type Compute = { [K in keyof A]: A[K] }; diff --git a/packages/xstate-store/test/store.test.ts b/packages/xstate-store/test/store.test.ts index d6b821172f..45082522b3 100644 --- a/packages/xstate-store/test/store.test.ts +++ b/packages/xstate-store/test/store.test.ts @@ -1,5 +1,5 @@ import { produce } from 'immer'; -import { createStore, createStoreWithProducer } from '../src/index.ts'; +import { Compute, createStore, createStoreWithProducer } from '../src/index.ts'; import { createBrowserInspector } from '@statelyai/inspect'; it('updates a store with an event without mutating original context', () => { @@ -385,3 +385,67 @@ it('effects can be enqueued', async () => { expect(store.getSnapshot().context.count).toEqual(0); }); + +describe('store.trigger', () => { + it('should allow triggering events with a fluent API', () => { + const store = createStore({ + context: { count: 0 }, + on: { + increment: (ctx, event: { by: number }) => ({ + count: ctx.count + event.by + }) + } + }); + + store.trigger.increment({ by: 5 }); + + expect(store.getSnapshot().context.count).toBe(5); + }); + + it('should provide type safety for event payloads', () => { + const store = createStore({ + context: { count: 0 }, + on: { + increment: (ctx, event: { by: number }) => ({ + count: ctx.count + event.by + }), + reset: () => ({ count: 0 }) + } + }); + + // @ts-expect-error - missing required 'by' property + store.trigger.increment({}); + + // @ts-expect-error - extra property not allowed + store.trigger.increment({ by: 1, extra: true }); + + // @ts-expect-error - unknown event + store.trigger.unknown({}); + + // Valid usage with no payload + store.trigger.reset(); + + // Valid usage with payload + store.trigger.increment({ by: 1 }); + }); + + it('should be equivalent to store.send', () => { + const store = createStore({ + context: { count: 0 }, + on: { + increment: (ctx, event: { by: number }) => ({ + count: ctx.count + event.by + }) + } + }); + + const sendSpy = jest.spyOn(store, 'send'); + + store.trigger.increment({ by: 5 }); + + expect(sendSpy).toHaveBeenCalledWith({ + type: 'increment', + by: 5 + }); + }); +}); From ad23c06fc32e3cdfe29e0da045fcd9c4d5528b38 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 30 Jan 2025 19:21:29 -0500 Subject: [PATCH 12/15] Add emits --- .changeset/wise-bikes-leave.md | 21 ++++++ packages/xstate-store/src/fromStore.ts | 31 +++++---- packages/xstate-store/src/store.ts | 18 ++++-- packages/xstate-store/src/types.ts | 8 ++- packages/xstate-store/test/fromStore.test.ts | 8 +-- packages/xstate-store/test/store.test.ts | 34 +++++----- packages/xstate-store/test/types.test.tsx | 68 ++++++-------------- 7 files changed, 95 insertions(+), 93 deletions(-) create mode 100644 .changeset/wise-bikes-leave.md diff --git a/.changeset/wise-bikes-leave.md b/.changeset/wise-bikes-leave.md new file mode 100644 index 0000000000..4da9c4de0b --- /dev/null +++ b/.changeset/wise-bikes-leave.md @@ -0,0 +1,21 @@ +--- +'@xstate/store': major +--- + +Emitted event types are now specified in functions on the `emits` property of the store definition: + +```ts +const store = createStore({ + // … + emits: { + increased: (payload: { upBy: number }) => {} + }, + on: { + inc: (ctx, ev: { by: number }, enq) => { + enq.emit.increased({ upBy: ev.by }); + + // … + } + } +}); +``` diff --git a/packages/xstate-store/src/fromStore.ts b/packages/xstate-store/src/fromStore.ts index 179605a32d..4c436a8699 100644 --- a/packages/xstate-store/src/fromStore.ts +++ b/packages/xstate-store/src/fromStore.ts @@ -60,23 +60,26 @@ export function fromStore< TContext extends StoreContext, TEventPayloadMap extends EventPayloadMap, TInput, - TTypes extends { emitted?: EventObject } ->( - config: { - context: ((input: TInput) => TContext) | TContext; - on: { - [K in keyof TEventPayloadMap & string]: StoreAssigner< - NoInfer, - { type: K } & TEventPayloadMap[K], - Cast - >; - }; - } & { types?: TTypes } -): StoreLogic< + TEmitted extends EventPayloadMap +>(config: { + context: ((input: TInput) => TContext) | TContext; + on: { + [K in keyof TEventPayloadMap & string]: StoreAssigner< + NoInfer, + { type: K } & TEventPayloadMap[K], + ExtractEventsFromPayloadMap + >; + }; + emits?: { + [K in keyof TEventPayloadMap & string]: ( + payload: { type: K } & TEventPayloadMap[K] + ) => void; + }; +}): StoreLogic< TContext, ExtractEventsFromPayloadMap, TInput, - TTypes['emitted'] extends EventObject ? TTypes['emitted'] : EventObject + ExtractEventsFromPayloadMap >; export function fromStore< TContext extends StoreContext, diff --git a/packages/xstate-store/src/store.ts b/packages/xstate-store/src/store.ts index 47fc8518d8..bd4a262c8e 100644 --- a/packages/xstate-store/src/store.ts +++ b/packages/xstate-store/src/store.ts @@ -236,6 +236,9 @@ type CreateStoreParameterTypes< > = [ definition: { context: TContext; + emits?: { + [K in keyof TEmitted & string]: (payload: TEmitted[K]) => void; + }; on: { [K in keyof TEventPayloadMap & string]: StoreAssigner< NoInfer, @@ -313,7 +316,7 @@ export const createStore: { < TContext extends StoreContext, TEventPayloadMap extends EventPayloadMap, - TEmitted extends EventObject + TEmitted extends EventPayloadMap >( ...args: CreateStoreParameterTypes ): CreateStoreReturnType; @@ -416,9 +419,16 @@ export function createStoreTransition< const effects: StoreEffect[] = []; const enqueue: EnqueueObject = { - emit: (ev: TEmitted) => { - effects.push(ev); - }, + emit: new Proxy({} as any, { + get: (_, eventType: string) => { + return (payload: any) => { + effects.push({ + type: eventType, + ...payload + }); + }; + } + }), effect: (fn) => { effects.push(fn); } diff --git a/packages/xstate-store/src/types.ts b/packages/xstate-store/src/types.ts index bd3e6d7721..58f654ae8c 100644 --- a/packages/xstate-store/src/types.ts +++ b/packages/xstate-store/src/types.ts @@ -6,8 +6,12 @@ export type ExtractEventsFromPayloadMap = Values<{ export type Recipe = (state: T) => TReturn; -export type EnqueueObject = { - emit: (ev: TEmitted) => void; +export type EnqueueObject = { + emit: { + [K in TEmittedEvent['type']]: ( + payload: Omit + ) => void; + }; effect: (fn: () => void) => void; }; diff --git a/packages/xstate-store/test/fromStore.test.ts b/packages/xstate-store/test/fromStore.test.ts index 1cacf94f8c..f89c1be159 100644 --- a/packages/xstate-store/test/fromStore.test.ts +++ b/packages/xstate-store/test/fromStore.test.ts @@ -54,13 +54,13 @@ describe('fromStore', () => { const spy = jest.fn(); const storeLogic = fromStore({ - types: { - emitted: {} as { type: 'increased'; upBy: number } - }, context: (count: number) => ({ count }), + emits: { + increased: (_: { upBy: number }) => {} + }, on: { inc: (ctx, ev: { by: number }, enq) => { - enq.emit({ type: 'increased', upBy: ev.by }); + enq.emit.increased({ upBy: ev.by }); return { ...ctx, count: ctx.count + ev.by diff --git a/packages/xstate-store/test/store.test.ts b/packages/xstate-store/test/store.test.ts index 45082522b3..c13ecefc10 100644 --- a/packages/xstate-store/test/store.test.ts +++ b/packages/xstate-store/test/store.test.ts @@ -258,18 +258,16 @@ it('inspection with @statelyai/inspect typechecks correctly', () => { }); it('emitted events can be subscribed to', () => { - const store = createStore< - { count: number }, - { inc: {} }, - { increased: { upBy: number } } - >({ + const store = createStore({ context: { count: 0 }, + emits: { + increased: (a: { upBy: number }) => {} + }, on: { inc: (ctx, _, enq) => { - enq.emit({ type: 'increased', upBy: 1 }); - + enq.emit.increased({ upBy: 1 }); return { ...ctx, count: ctx.count + 1 @@ -288,17 +286,16 @@ it('emitted events can be subscribed to', () => { }); it('emitted events can be unsubscribed to', () => { - const store = createStore< - { count: number }, - { inc: {} }, - { increased: { upBy: number } } - >({ + const store = createStore({ context: { count: 0 }, + emits: { + increased: (_: { upBy: number }) => {} + }, on: { inc: (ctx, _, enq) => { - enq.emit({ type: 'increased', upBy: 1 }); + enq.emit.increased({ upBy: 1 }); return { ...ctx, @@ -321,17 +318,16 @@ it('emitted events can be unsubscribed to', () => { }); it('emitted events occur after the snapshot is updated', () => { - const store = createStore< - { count: number }, - { inc: {} }, - { increased: { upBy: number } } - >({ + const store = createStore({ context: { count: 0 }, + emits: { + increased: (_: { upBy: number }) => {} + }, on: { inc: (ctx, _, enq) => { - enq.emit({ type: 'increased', upBy: 1 }); + enq.emit.increased({ upBy: 1 }); return { ...ctx, diff --git a/packages/xstate-store/test/types.test.tsx b/packages/xstate-store/test/types.test.tsx index 877b4babf7..1e4a0c7281 100644 --- a/packages/xstate-store/test/types.test.tsx +++ b/packages/xstate-store/test/types.test.tsx @@ -2,20 +2,14 @@ import { createStore } from '../src/index'; describe('emitted', () => { it('can emit a known event', () => { - createStore< - {}, - { - inc: { upBy: number }; - }, - { - increased: { upBy: number }; - decreased: { downBy: number }; - } - >({ + createStore({ context: {}, + emits: { + increased: (_: { upBy: number }) => {} + }, on: { inc: (ctx, _, enq) => { - enq.emit({ type: 'increased', upBy: 1 }); + enq.emit.increased({ upBy: 1 }); return ctx; } } @@ -23,23 +17,17 @@ describe('emitted', () => { }); it("can't emit an unknown event", () => { - createStore< - {}, - { - inc: { upBy: number }; - }, - { - increased: { upBy: number }; - decreased: { downBy: number }; - } - >({ + createStore({ context: {}, + emits: { + increased: (_: { upBy: number }) => {}, + decreased: (_: { downBy: number }) => {} + }, on: { inc: (ctx, _, enq) => { - enq.emit({ + enq.emit // @ts-expect-error - type: 'unknown' - }); + .unknown(); return ctx; } } @@ -47,21 +35,15 @@ describe('emitted', () => { }); it("can't emit a known event with wrong payload", () => { - createStore< - {}, - { - inc: { upBy: number }; - }, - { - increased: { upBy: number }; - decreased: { downBy: number }; - } - >({ + createStore({ context: {}, + emits: { + increased: (_: { upBy: number }) => {}, + decreased: (_: { downBy: number }) => {} + }, on: { inc: (ctx, _, enq) => { - enq.emit({ - type: 'increased', + enq.emit.increased({ // @ts-expect-error upBy: 'bazinga' }); @@ -71,20 +53,6 @@ describe('emitted', () => { }); }); - it('can emit an event when emitted events are unknown', () => { - createStore({ - context: {}, - on: { - inc: (ctx, _, enq) => { - enq.emit({ - type: 'unknown' - }); - return ctx; - } - } - }); - }); - it('can subscribe to a known event', () => { const store = createStore< {}, From babb74d462d68d6f279de964fc08d5f508af4378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 31 Jan 2025 23:35:59 +0100 Subject: [PATCH 13/15] Fixed TS issue --- packages/xstate-store/src/types.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/xstate-store/src/types.ts b/packages/xstate-store/src/types.ts index 58f654ae8c..b1baacee4f 100644 --- a/packages/xstate-store/src/types.ts +++ b/packages/xstate-store/src/types.ts @@ -8,9 +8,7 @@ export type Recipe = (state: T) => TReturn; export type EnqueueObject = { emit: { - [K in TEmittedEvent['type']]: ( - payload: Omit - ) => void; + [E in TEmittedEvent as E['type']]: (payload: Omit) => void; }; effect: (fn: () => void) => void; }; @@ -109,11 +107,11 @@ export interface Store< * ``` */ trigger: { - [K in TEvent['type'] & string]: IsEmptyObject< - Omit<{ type: K } & TEvent, 'type'> + [E in TEvent as E['type'] & string]: IsEmptyObject< + Omit > extends true - ? () => Omit<{ type: K } & TEvent, 'type'> - : (eventPayload: Omit<{ type: K } & TEvent, 'type'>) => void; + ? () => Omit + : (eventPayload: Omit) => void; }; } @@ -314,5 +312,5 @@ export type Prop = K extends keyof T ? T[K] : never; export type Cast = A extends B ? A : B; export type EventMap = { - [K in TEvent['type']]: TEvent & { type: K }; + [E in TEvent as E['type']]: E; }; From 610ca159c4c7e5f216f9f924016e34d11755f6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 31 Jan 2025 23:47:02 +0100 Subject: [PATCH 14/15] use correct types --- packages/xstate-store/src/fromStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/xstate-store/src/fromStore.ts b/packages/xstate-store/src/fromStore.ts index 4c436a8699..2497f3094f 100644 --- a/packages/xstate-store/src/fromStore.ts +++ b/packages/xstate-store/src/fromStore.ts @@ -71,8 +71,8 @@ export function fromStore< >; }; emits?: { - [K in keyof TEventPayloadMap & string]: ( - payload: { type: K } & TEventPayloadMap[K] + [K in keyof TEmitted & string]: ( + payload: { type: K } & TEmitted[K] ) => void; }; }): StoreLogic< From 80fe5932f9653da173be28508fe7b3f2db94bee2 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 1 Feb 2025 12:54:17 -0500 Subject: [PATCH 15/15] Remove changeset --- .changeset/big-schools-hang.md | 37 ---------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 .changeset/big-schools-hang.md diff --git a/.changeset/big-schools-hang.md b/.changeset/big-schools-hang.md deleted file mode 100644 index 900309da76..0000000000 --- a/.changeset/big-schools-hang.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@xstate/store': major ---- - -Type parameters should now be explicitly provided to `createStore` and `createStoreWithProducer`. For sent and emitted events, the an `EventPayloadMap` should be provided, which is a map of event types to their payloads. - -```ts -createStore< - // Context - { - count: number; - }, - // Sent events - { - inc: { - by: number; - }; - }, - // Emitted events - { - increased: { - upBy: number; - }; - } ->({ - context: { count: 0 }, - on: { - inc: (ctx, event, enq) => { - enq.emit({ type: 'increased', upBy: event.by }); - return { - ...ctx, - count: ctx.count + event.by - }; - } - } -}); -```