Skip to content

Commit

Permalink
[@xstate/store] Event emitter (#5064)
Browse files Browse the repository at this point in the history
* Add emitting

* Zod?

* Types or schema

* Export setup + changeset

* Update packages/xstate-store/src/setup.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Fix types

* Overload hell

* WIP

* Delete setup, update changeset

* Add sub/unsub tests

* Update packages/xstate-store/src/store.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/xstate-store/test/fromStore.test.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/xstate-store/src/fromStore.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/xstate-store/src/fromStore.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/xstate-store/src/fromStore.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Address PR comments

* Fix types with fromStore

* Remove schemas (separate PR)

* fix small things

* remove redundant `NoInfer`

* tweak types

* fix knip

* support `TTypes['events']`

* Update .changeset/silver-maps-grab.md

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Revert "support `TTypes['events']`"

This reverts commit 3ffa8dc.

* Simplify overload

* Update fromStore

* Changeset

* Fix emitted event ordering issue

* Emitted after observers

* move type tests to a separate file

* add missing import, oops

* use a spy

* fixed test title

---------

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
davidkpiano and Andarist authored Sep 16, 2024
1 parent f89de0f commit 84aca37
Show file tree
Hide file tree
Showing 10 changed files with 709 additions and 90 deletions.
29 changes: 29 additions & 0 deletions .changeset/moody-days-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@xstate/store': minor
---

There is a new single-argument config API for `createStore(config)`:

```ts
const store = createStore({
// Types (optional)
types: {
emitted: {} as { type: 'incremented' }
},

// Context
context: { count: 0 },

// Transitions
on: {
inc: (context, event: { by: number }, enq) => {
enq.emit({ type: 'incremented' });

return { count: context.count + event.by };
},
dec: (context, event: { by: number }) => ({
count: context.count - event.by
})
}
});
```
26 changes: 26 additions & 0 deletions .changeset/silver-maps-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
'@xstate/store': minor
---

You can now emit events from a store:

```ts
import { createStore } from '@xstate/store';

const store = createStore({
context: {
count: 0
},
on: {
increment: (context, event, { emit }) => {
emit({ type: 'incremented' });
return { count: context.count + 1 };
}
}
});

store.on('incremented', () => {
console.log('incremented!');
});
```

138 changes: 128 additions & 10 deletions packages/xstate-store/src/fromStore.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import { createStore, createStoreTransition } from './store';
import { ActorLogic, Cast } from 'xstate';
import { createStoreTransition, TransitionsFromEventPayloadMap } from './store';
import {
EventPayloadMap,
StoreContext,
Snapshot,
StoreSnapshot,
Snapshot
EventObject,
ExtractEventsFromPayloadMap,
StoreAssigner,
StorePropertyAssigner
} from './types';

type StoreLogic<
TContext extends StoreContext,
TEvent extends EventObject,
TInput,
TEmitted extends EventObject
> = ActorLogic<StoreSnapshot<TContext>, TEvent, TInput, any, TEmitted>;

/**
* An actor logic creator which creates store [actor
* logic](https://stately.ai/docs/actors#actor-logic) for use with XState.
Expand All @@ -22,15 +34,121 @@ export function fromStore<
TInput
>(
initialContext: ((input: TInput) => TContext) | TContext,
transitions: Parameters<typeof createStore<TContext, TEventPayloadMap>>[1]
) {
const transition = createStoreTransition<TContext, TEventPayloadMap>(
transitions
);
transitions: TransitionsFromEventPayloadMap<
TEventPayloadMap,
NoInfer<TContext>,
EventObject
>
): StoreLogic<
TContext,
ExtractEventsFromPayloadMap<TEventPayloadMap>,
TInput,
EventObject
>;

/**
* An actor logic creator which creates store [actor
* logic](https://stately.ai/docs/actors#actor-logic) for use with XState.
*
* @param config An object containing the store configuration
* @param config.context The initial context for the store, either a function
* that returns context based on input, or the context itself
* @param config.on An object defining the transitions for different event types
* @param config.types Optional object to define custom event types
* @returns An actor logic creator function that creates store actor logic
*/
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<TContext>,
{ type: K } & TEventPayloadMap[K],
Cast<TTypes['emitted'], EventObject>
>
| StorePropertyAssigner<
NoInfer<TContext>,
{ type: K } & TEventPayloadMap[K],
Cast<TTypes['emitted'], EventObject>
>;
};
} & { types?: TTypes }
): StoreLogic<
TContext,
ExtractEventsFromPayloadMap<TEventPayloadMap>,
TInput,
TTypes['emitted'] extends EventObject ? TTypes['emitted'] : EventObject
>;
export function fromStore<
TContext extends StoreContext,
TEventPayloadMap extends EventPayloadMap,
TInput,
TTypes extends { emitted?: EventObject }
>(
initialContextOrObj:
| ((input: TInput) => TContext)
| TContext
| ({
context: ((input: TInput) => TContext) | TContext;
on: {
[K in keyof TEventPayloadMap & string]:
| StoreAssigner<
NoInfer<TContext>,
{ type: K } & TEventPayloadMap[K],
Cast<TTypes['emitted'], EventObject>
>
| StorePropertyAssigner<
NoInfer<TContext>,
{ type: K } & TEventPayloadMap[K],
Cast<TTypes['emitted'], EventObject>
>;
};
} & { types?: TTypes }),
transitions?: TransitionsFromEventPayloadMap<
TEventPayloadMap,
NoInfer<TContext>,
EventObject
>
): StoreLogic<
TContext,
ExtractEventsFromPayloadMap<TEventPayloadMap>,
TInput,
TTypes['emitted'] extends EventObject ? TTypes['emitted'] : EventObject
> {
let initialContext: ((input: TInput) => TContext) | TContext;
let transitionsObj: TransitionsFromEventPayloadMap<
TEventPayloadMap,
NoInfer<TContext>,
EventObject
>;

if (
typeof initialContextOrObj === 'object' &&
'context' in initialContextOrObj
) {
initialContext = initialContextOrObj.context;
transitionsObj = initialContextOrObj.on;
} else {
initialContext = initialContextOrObj;
transitionsObj = transitions!;
}

const transition = createStoreTransition(transitionsObj);
return {
transition,
start: () => {},
getInitialSnapshot: (_: any, input: TInput) => {
transition: (snapshot, event, actorScope) => {
const [nextSnapshot, emittedEvents] = transition(snapshot, event);

emittedEvents.forEach(actorScope.emit);

return nextSnapshot;
},
getInitialSnapshot: (_, input: TInput) => {
return {
status: 'active',
context:
Expand Down
6 changes: 3 additions & 3 deletions packages/xstate-store/src/react.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useCallback, useRef, useSyncExternalStore } from 'react';
import { Store, SnapshotFromStore } from './types';
import { Store, SnapshotFromStore, AnyStore } from './types';

function defaultCompare<T>(a: T | undefined, b: T) {
return a === b;
}

function useSelectorWithCompare<TStore extends Store<any, any>, T>(
function useSelectorWithCompare<TStore extends AnyStore, T>(
selector: (snapshot: SnapshotFromStore<TStore>) => T,
compare: (a: T | undefined, b: T) => boolean
): (snapshot: SnapshotFromStore<TStore>) => T {
Expand Down Expand Up @@ -40,7 +40,7 @@ function useSelectorWithCompare<TStore extends Store<any, any>, T>(
* previous value
* @returns The selected value
*/
export function useSelector<TStore extends Store<any, any>, T>(
export function useSelector<TStore extends AnyStore, T>(
store: TStore,
selector: (snapshot: SnapshotFromStore<TStore>) => T,
compare: (a: T | undefined, b: T) => boolean = defaultCompare
Expand Down
6 changes: 3 additions & 3 deletions packages/xstate-store/src/solid.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* @jsxImportSource solid-js */
import { createEffect, createSignal, onCleanup } from 'solid-js';
import type { Store, SnapshotFromStore } from './types';
import type { Store, SnapshotFromStore, AnyStore } from './types';

function defaultCompare<T>(a: T | undefined, b: T) {
return a === b;
}

function useSelectorWithCompare<TStore extends Store<any, any>, T>(
function useSelectorWithCompare<TStore extends AnyStore, T>(
selector: (snapshot: SnapshotFromStore<TStore>) => T,
compare: (a: T | undefined, b: T) => boolean
): (snapshot: SnapshotFromStore<TStore>) => T {
Expand Down Expand Up @@ -53,7 +53,7 @@ function useSelectorWithCompare<TStore extends Store<any, any>, T>(
* previously selected value
* @returns A read-only signal of the selected value
*/
export function useSelector<TStore extends Store<any, any>, T>(
export function useSelector<TStore extends AnyStore, T>(
store: TStore,
selector: (snapshot: SnapshotFromStore<TStore>) => T,
compare: (a: T | undefined, b: T) => boolean = defaultCompare
Expand Down
Loading

0 comments on commit 84aca37

Please sign in to comment.