-
I'm trying to wrap my head around xState and don't understand something. In functional languages I define states using discriminated unions like...
So each state has unique data for that state. So if I want to draw the Error state I can write "Something went wrong: {Error}" In xState it doesn't seem like this is how it works. It looks like one Context is shared by all the possible states. So an Error message exists for all states, though it might be null or undefined for some. Is this correct? And if so, is that a technical limitation or the way we really want it to work? As a side note I did build my code using a hand made type-safe Typescript function. Action -> State -> State. I sprinkled in some side effects. It works but it was super tedious to write with all the Switch statements. Doesn't do hierarchical or history or visualization but it works. So I thought maybe xState would make this easier (and a lot shorter) and ran into the conceptual problem described above. |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 11 replies
-
I'm not familiar enough with functional languages to know how this works there but in TypeScript this ain't super trivial right now. You can actually use typestates to achieve what you want but note that this is not type-safe. |
Beta Was this translation helpful? Give feedback.
-
In typescript You can define something called discrimination union and dis is usually helpful to define state and events with different payloads export type AuthorizeMerchantState =
| { type: 'LOGGED_OUT' }
| { type: 'PARAM'; code: string }
| { type: 'LOADING_LOGIN' }
| { type: 'LOGIN_FAILURE' }
| { type: 'LOGGED_IN_TO_SYSTEM'; user: User; claims: ParsedToken }; then in Your (react) component: const [state, setState] = useState<AuthorizeMerchantState>({
type: 'LOGGED_OUT'
}); and define actions: const check = async (id: string) => {
assertState(state, `PARAM`);
setState({ type: 'LOADING_LOGIN' });
try {
const { token } = await MerchantAuthService.signIn(id);
console.debug({ token });
const result = await AuthService.customSignIn(token);
if (result === undefined) {
setState({ type: 'LOGIN_FAILURE' });
navigate(AppRoutes.SIGN_IN_INFO);
return;
}
setState({ type: 'LOGGED_IN_TO_SYSTEM', user: result.user, claims: result.claims });
navigate(AppRoutes.LANDING_PAGE);
} catch (e) {
navigate(AppRoutes.SIGN_IN_INFO);
}
}; actions can use State guard export function assertState<TType extends string>(
state: { type: string },
...expectedTypes: TType[]
): asserts state is StateMember<TType> {
if (!expectedTypes.includes(state.type as TType)) {
throw new Error(`Invalid state ${state.type} (expected one of: ${expectedTypes})`);
}
} around this states is easy to build component with view composition: switch (state.type) {
case 'SCANNING':
case 'POSTER_SCANNED':
case 'CONTAINER_SCANNED': {
return <View.Scanning {...state} />;
}
case 'FULLFILLED':
return <View.Fullified {...state} />;
case 'RETURN_SUCCESS':
return (
<View.Success
{...state}
onSuccess={onSuccess}
/>
);
case 'RETURN_ERROR':
return <View.ReturnError {...state} />;
case 'LOADING':
return <Loader my="40vh" />;
default:
return null;
} Another useful case can be building aggregate in Event Driven approach like in emmet where around this pattern You can easily buld Decider Evolve etc So individual context per state is really useful to avoid primitive obsession, and manually reseting payload state. |
Beta Was this translation helpful? Give feedback.
-
Yes I know TypeScript allows discriminated unions. The question is whether XState allows different state types per state to ensure that if you are in state A the state data is guaranteed to be a different branch of the union than in state B. |
Beta Was this translation helpful? Give feedback.
I'm not familiar enough with functional languages to know how this works there but in TypeScript this ain't super trivial right now. You can actually use typestates to achieve what you want but note that this is not type-safe.