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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 })
}
});
```
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 });

// …
}
}
});
```
66 changes: 31 additions & 35 deletions packages/xstate-store/src/fromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import {
StoreSnapshot,
EventObject,
ExtractEventsFromPayloadMap,
StoreAssigner,
StorePropertyAssigner
StoreAssigner
} from './types';

type StoreLogic<
Expand Down Expand Up @@ -61,29 +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<TContext>,
{ type: K } & TEventPayloadMap[K],
Cast<TTypes['emitted'], EventObject>
>
| StorePropertyAssigner<
NoInfer<TContext>,
{ type: K } & TEventPayloadMap[K],
Cast<TTypes['emitted'], EventObject>
>;
};
} & { types?: TTypes }
): StoreLogic<
TEmitted extends EventPayloadMap
>(config: {
context: ((input: TInput) => TContext) | TContext;
on: {
[K in keyof TEventPayloadMap & string]: StoreAssigner<
NoInfer<TContext>,
{ type: K } & TEventPayloadMap[K],
ExtractEventsFromPayloadMap<TEmitted>
>;
};
emits?: {
[K in keyof TEmitted & string]: (
payload: { type: K } & TEmitted[K]
) => void;
};
}): StoreLogic<
TContext,
ExtractEventsFromPayloadMap<TEventPayloadMap>,
TInput,
TTypes['emitted'] extends EventObject ? TTypes['emitted'] : EventObject
ExtractEventsFromPayloadMap<TEmitted>
>;
export function fromStore<
TContext extends StoreContext,
Expand All @@ -97,17 +93,11 @@ export function fromStore<
| ({
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>
>;
[K in keyof TEventPayloadMap & string]: StoreAssigner<
NoInfer<TContext>,
{ type: K } & TEventPayloadMap[K],
Cast<TTypes['emitted'], EventObject>
>;
};
} & { types?: TTypes }),
transitions?: TransitionsFromEventPayloadMap<
Expand Down Expand Up @@ -142,9 +132,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;
},
Expand Down
Loading