Skip to content

[@xstate/store] createStoreLogic and selectors#5490

Open
davidkpiano wants to merge 6 commits intomainfrom
davidkpiano/create-store-logic-selectors
Open

[@xstate/store] createStoreLogic and selectors#5490
davidkpiano wants to merge 6 commits intomainfrom
davidkpiano/create-store-logic-selectors

Conversation

@davidkpiano
Copy link
Copy Markdown
Member

Added createStoreLogic for reusable store definitions with input and selectors support for both createStore and createStoreLogic.

const counterLogic = createStoreLogic({
  context: (input: { initialCount: number }) => ({
    count: input.initialCount
  }),
  on: {
    inc: (ctx) => ({ count: ctx.count + 1 })
  },
  selectors: {
    doubled: (ctx) => ctx.count * 2
  }
});

const counter1 = counterLogic.createStore({ initialCount: 42 });
const counter2 = counterLogic.createStore({ initialCount: 0 });

counter1.selectors.doubled.get(); // 84

Selectors are reactive ReadonlyAtoms (powered by store.select()), composable with createAtom, and preserved through .with() extensions.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 27, 2026

🦋 Changeset detected

Latest commit: 81648e8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
@xstate/store Minor
@xstate/store-angular Patch
@xstate/store-preact Patch
@xstate/store-react Patch
@xstate/store-solid Patch
@xstate/store-svelte Patch
@xstate/store-vue Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@davidkpiano davidkpiano requested a review from Andarist March 27, 2026 11:56
Copy link
Copy Markdown
Collaborator

@Andarist Andarist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that all of my comments are just questions, not suggestions

Comment on lines +68 to +71
storeWithSelectors.with = ((extension: any) => {
const extended = originalWith(extension);
return attachSelectors(extended, selectorsConfig);
}) as any;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: will this work properly with multi-layer .with calls? like x.with().with().with(). It feels like it will lead to calling attachSelectors for each of those layers and I wonder if that's a) optimal b) correct

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on that point, why emits and selectors are threaded through things differently? emits go through createStoreCore and selectors go through attachSelectors

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: will this work properly with multi-layer .with calls? like x.with().with().with(). It feels like it will lead to calling attachSelectors for each of those layers and I wonder if that's a) optimal b) correct

It's correct, and yeah the intermediate selector atoms get GC'd which is slightly wasteful but correct & simple. Also selectors are after-the-fact (they observe the store; they're not part of the store), and we shouldn't add them to storeLogic

Comment on lines +350 to +352
| (StoreConfig<any, any, any> & {
selectors?: Record<string, (context: any) => any>;
})
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: related to some of the other questions - why selectors are not part of the StoreConfig?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeps it additive so we don't pollute the StoreConfig interface- createStoreConfig and fromStore don't need it at the moment. We can move it in later if we decide to make selectors a broader XState concept maybe?

davidkpiano and others added 3 commits March 27, 2026 12:50
Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
- Remove unnecessary .bind(store) on .with() (uses closures, not this)
- Remove redundant intersection on selectors type
- Split createStoreLogic into 4 overloads to fix TContext inference
  in selectors for both static context and input-function cases
- Keep selectors outside StoreConfig (correct separation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sasivarnan
Copy link
Copy Markdown

Thank you for implementing this feature. I am a primary user of the ability to create multiple store instances from a store definition.

Could you confirm if we are planning to use the value returned by createStoreLogic to initialize a local store that is created with useStore? This way we can define the local store in an external file.

@davidkpiano
Copy link
Copy Markdown
Member Author

Could you confirm if we are planning to use the value returned by createStoreLogic to initialize a local store that is created with useStore? This way we can define the local store in an external file.

Good question; not yet but it should.

Comment on lines +537 to +541
createStore: [TInput] extends [void]
? () => StoreWithSelectors<TContext, TEventPayloadMap, TEmitted, TSelectors>
: (
input: TInput
) => StoreWithSelectors<TContext, TEventPayloadMap, TEmitted, TSelectors>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'd usually only allow a dynamic context to be created based on the provided input. Creating the whole store feels like a divergence from the other APIs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants