Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[@xstate/store] v3 #5175

Merged
merged 26 commits into from
Feb 9, 2025
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
49cf959
Only complete assigners are allowed
davidkpiano Jan 16, 2025
a0b5dd2
`createStore` only takes a single { context, on } argument now
davidkpiano Jan 19, 2025
4f3d224
createStoreWithProducer
davidkpiano Jan 19, 2025
4d2ddf0
Enhance store functionality by introducing effects in event handling
davidkpiano Jan 22, 2025
d55e963
use overload trick 🫠
Andarist Jan 22, 2025
73c9489
Fix fromStore
davidkpiano Jan 23, 2025
3d2cded
Update changesets
davidkpiano Jan 25, 2025
cfe28e6
Add support for type parameters
davidkpiano Jan 25, 2025
c0efebf
Revert "Add support for type parameters"
davidkpiano Jan 26, 2025
8528460
Refactor store type parameters to improve type safety and flexibility
davidkpiano Jan 26, 2025
008badf
Add trigger + test + changeset
davidkpiano Jan 26, 2025
370796e
Merge branch 'main' into davidkpiano/store-v3
davidkpiano Jan 30, 2025
ad23c06
Add emits
davidkpiano Jan 31, 2025
babb74d
Fixed TS issue
Andarist Jan 31, 2025
610ca15
use correct types
Andarist Jan 31, 2025
80fe593
Remove changeset
davidkpiano Feb 1, 2025
b325d68
use util
Andarist Feb 2, 2025
0433e3d
remove redundant test
Andarist Feb 2, 2025
0e53d88
tweak things
Andarist Feb 2, 2025
0ad2815
remove commented out code
Andarist Feb 2, 2025
cb03154
Quick typestates test
davidkpiano Feb 3, 2025
3672932
Update tests
davidkpiano Feb 8, 2025
5935e0c
Update fromStore
davidkpiano Feb 8, 2025
e6c2523
Fix tests
davidkpiano Feb 8, 2025
2276832
Update packages/xstate-store/test/fromStore.test.ts
davidkpiano Feb 9, 2025
c2ea8e3
Improve jsdoc comments and test
davidkpiano Feb 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/five-walls-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@xstate/store': major
---

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 })
}
});
```
24 changes: 24 additions & 0 deletions .changeset/great-candles-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@xstate/store': major
---

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 })
}
});
```
17 changes: 17 additions & 0 deletions .changeset/mean-taxis-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@xstate/store': major
---

The `fromStore(config)` function now only supports a single config object argument.

```ts
const storeLogic = fromStore({
context: (input: { initialCount: number }) => ({ count: input.initialCount }),
on: {
inc: (ctx, ev: { by: number }) => ({
...ctx,
count: ctx.count + ev.by
})
}
});
```
24 changes: 24 additions & 0 deletions .changeset/quick-bears-swim.md
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 28 additions & 0 deletions .changeset/spotty-moose-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@xstate/store': major
---

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 })
}
});
```
19 changes: 19 additions & 0 deletions .changeset/thick-paws-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@xstate/store': major
---

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 })
}
})
```
21 changes: 21 additions & 0 deletions .changeset/wise-bikes-leave.md
Original file line number Diff line number Diff line change
@@ -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 });

//
}
}
});
```
140 changes: 34 additions & 106 deletions packages/xstate-store/src/fromStore.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { ActorLogic, Cast } from 'xstate';
import { ActorLogic } from 'xstate';
import { createStoreTransition, TransitionsFromEventPayloadMap } from './store';
import {
EventPayloadMap,
StoreContext,
Snapshot,
StoreSnapshot,
EventObject,
ExtractEventsFromPayloadMap,
StoreAssigner,
StorePropertyAssigner
ExtractEvents,
StoreAssigner
} from './types';

type StoreLogic<
Expand All @@ -18,34 +17,6 @@ type StoreLogic<
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.
*
* @param initialContext The initial context for the store, either a function
* that returns context based on input, or the context itself
* @param transitions The transitions object defining how the context updates
* due to events
* @returns An actor logic creator function that creates store actor logic
*/
export function fromStore<
TContext extends StoreContext,
TEventPayloadMap extends EventPayloadMap,
TInput
>(
initialContext: ((input: TInput) => TContext) | TContext,
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.
Expand All @@ -54,97 +25,54 @@ export function fromStore<
* @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
* @param config.emits Optional object to define emitted event handlers
* @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<
TEmitted extends EventPayloadMap
>(config: {
context: ((input: TInput) => TContext) | TContext;
on: {
[K in keyof TEventPayloadMap & string]: StoreAssigner<
NoInfer<TContext>,
{ type: K } & TEventPayloadMap[K],
ExtractEvents<TEmitted>
>;
};
emits?: {
[K in keyof TEmitted & string]: (
payload: { type: K } & TEmitted[K]
) => void;
};
}): StoreLogic<
TContext,
ExtractEventsFromPayloadMap<TEventPayloadMap>,
ExtractEvents<TEventPayloadMap>,
TInput,
TTypes['emitted'] extends EventObject ? TTypes['emitted'] : EventObject
ExtractEvents<TEmitted>
> {
let initialContext: ((input: TInput) => TContext) | TContext;
let transitionsObj: TransitionsFromEventPayloadMap<
const initialContext: ((input: TInput) => TContext) | TContext =
config.context;
const transitionsObj: TransitionsFromEventPayloadMap<
TEventPayloadMap,
NoInfer<TContext>,
EventObject
>;

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

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 as ExtractEvents<TEmitted>);
}
}

return nextSnapshot;
},
Expand Down
Loading