Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Reusable states returned from function#5336

Unanswered
jancewicz asked this question inQ&A
Jul 25, 2025· 1 comments· 8 replies
Discussion options

Hi everyone,

I wonder if anyone has tried to declare reusable states that are returned by a TS function? I see that on my project I have similar scenarios where for example, from init state I always go to the state that fetch some list from API, asign it to context variable and in onDone it goes to listLoaded state or listLoadError. I have exactly same pattern across 3 machines and my idea was to just create a function that will return such state as it's an object.

After full day of fighting typescript, I find it very difficult to properly type such function. Despite the fact that calling such function in state machine fully works, the typescript cannot calm down for a sec and always see some deep type being invalid.

My example would go like this:

functiongetLoadingListState(){return{tags:["loading"],invoke:{src:"getList",onDone:{target:"listLoaded",actions:assign({entries:({ event})=>event.output}),},onError:{target:"listLoadError",actions:({ event, context})=>{context.toastMessageService.error(errorMsg(event),"Error on loading list")//some custom logic for displaying toast with err msg},},},},}

Then in my machines all I want to do is in states property be able to write:{ loadingList: getLoadingListState() }

How to type such function? What I see is that each machine needs whole context interface to be provided, but event with this I'm still fighting with type system,. What do i need to do to make this function generic across 3 different machines assuming context variable name for list or actors names are going to be the same.

I wonder if anyone had similar expirience or maybe has appropriate solution to this.

You must be logged in to vote

Replies: 1 comment 8 replies

Comment options

I've investigated this topic in depth, and from my experience, XState isn't really designed to support re-usability between machines (@davidkpiano correct me if I'm wrong). Trying to abstract shared states across machines quickly becomes a nightmare due to the heavy reliance on TypeScript inference. The generic types get out of hand, easily exceeding 10 required generic parameters to pass down if you want proper typing across different machines with different contexts and events.

I also attempted to build utility functions for actions and actors, hoping to inject them into machines for reuse. While it can technically work at runtime, TypeScript constantly complained about mismatched or missing types. Even with strict alignment on context shapes and event types, I found the DX was not worth it, especially when one small divergence between machines required a full reworking of the abstraction.

In the end, I abandoned XState for this very reason. I went with my own implementation, less complex with only the features I need. There is also this big caveat that XState (v5) has a single shared context between all the states, making it unsafe to use (paradoxical). I like to call it the Schrodinger context. At this time and state, the value might be there or not.

It is too painful to fight the type system.

You must be logged in to vote
8 replies
@davidkpiano
Comment options

@jancewicz Yes, something like that. In v6, there will be much fewer inferred types to worry about, so we can definitely improve the DX around "modularizing" a state machine.

@maksbialas
Comment options

Indeed, having some kind of reusability becomes a must once the project becomes large and you start repeating yourself a lot between your machines. BTW do you guys have some idea how to mitigate the consequences of having a single shared context between states (besides being careful when designing your states and transitions)?

@RemyMachado
Comment options

Hey@davidkpiano,

Can you share more about your solution? We're working on XState v6 and I want to see if there are some parallels between what we're planning and what you came up with.

Of course. Here are the core features I wanted to achieve:

  1. Type-safe context: For any given state, the context shape must be known precisely.
  2. Type-safe machine definition: No room for errors when declaring states, events, transitions, or handlers.
  3. Reusability of modules, including:
    • Logic (actions, handlers)
    • States
    • Events

I built a system based on two principles:

  1. Each state is responsible for constructing the next.
  2. Each state is an independent module.

To make this work, each module’s handlers are implemented as factory functions that accept the dependencies they need. This decouples handlers from both the previous and next context shapes.

Here's a simplified version of the approach (in practice, everything is fully type-generic):

typeNewState={name:string;context:Record<string,unknown>;};typeCreateNextStateFn=(handlerResult:Foo,previousContext:Record<string,unknown>)=>NewState;functioncreateHandlerFoo(createNextState:CreateNextStateFn){return(event:FooEvent,context:Record<string,unknown>)=>{constresult=handlerFoo(event,context);returncreateNextState(result,context);};}constmachineDefinition={[STATE.FOO]:{events:{[EVENT.DO_FOO]:createHandlerFoo((fooResult:Foo)=>({name:STATE.BAR,context:{bar:fooResult},})),},},[STATE.BAR]:{events:{[EVENT.DO_BAR]:createHandlerBar((barResult:Bar)=>({name:STATE.FOO,context:{foo:barResult},})),},},};

The main difficulty I encountered is sharing data between distant states, since there is no more shared context. Currently, I'm drilling/accumulating on each intermediary state, but it's far from ideal.

Would love to hear your thoughts. Also curious how you're approaching this kind of modularity and type safety for V6.

@RemyMachado
Comment options

@davidkpiano Just checking-in. I'm really curious about your input. I'm not satisfied at all with the context drilling. I'm curious about your thoughts on this.

@davidkpiano
Comment options

@RemyMachado The tentative solution that I'm working on for XState v6 is doubling down on thetypestate (discriminated union) approach, with explicit schemas:

// TENTATIVE FUTURE APIconstcontextSchema=z.object({userId:z.string().nullable()});constmachine=createMachine({schemas:{context:contextSchema},// ...states:{userLoaded:{schemas:{context:contextSchema.extend({userId:z.string()})}}}});// ...if(state.matches('userLoaded')){state.context.userId;// string}else{state.context.userId;// string or null}
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Category
Q&A
Labels
None yet
4 participants
@jancewicz@davidkpiano@maksbialas@RemyMachado

[8]ページ先頭

©2009-2025 Movatter.jp