Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork1.3k
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
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: 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. |
BetaWas this translation helpful?Give feedback.
All reactions
Replies: 1 comment 8 replies
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
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. |
BetaWas this translation helpful?Give feedback.
All reactions
-
@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. |
BetaWas this translation helpful?Give feedback.
All reactions
❤️ 1
-
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)? |
BetaWas this translation helpful?Give feedback.
All reactions
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
Hey@davidkpiano,
Of course. Here are the core features I wanted to achieve:
I built a system based on two principles:
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. |
BetaWas this translation helpful?Give feedback.
All reactions
-
@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. |
BetaWas this translation helpful?Give feedback.
All reactions
-
@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} |
BetaWas this translation helpful?Give feedback.
All reactions
❤️ 2👀 1