Xstate: Xstate <> React <> TypeScript integration snippets

Created on 10 Mar 2020  路  4Comments  路  Source: davidkpiano/xstate

Bug or feature request?

Documentation / feature proposal

Description:

Hi, I just implemented my first state machine on my React/TypeScript project with xstate ! I followed the documentation but I faced some issues. I would like to present some technical solutions and code snippets that I wrote to overcome those issues:

  • createMachineContext instead of createContext to overcome TypeScript inference issues
  • useMachineContext instead of useContext to avoid superfluous undefined type checking

If they are relevant I would be happy to open a PR :) Otherwise I welcome any feedback.

Using React context to share a state machine

I created the state machine at a quite high level and I didn't want to drill props through 4-5 components, so I figured out I would put the machine in React context. However I found it quite painful to type the context.

export const MyMachineContext = createContext(null);
// infered type by TypeScript: const MyMachineContext: React.Context<null>

Initially the context is null so the type can't be inferred automatically by TypeScript. I created this utils to create the context:

import { createContext } from 'react';

import { EventObject, Interpreter, State } from 'xstate';

export type StateType<ContextType, EventType extends EventObject> = State<
  ContextType,
  EventType,
  any
>;
export type SendType<ContextType, EventType extends EventObject> = Interpreter<
  ContextType,
  any,
  EventType
>['send'];
export const createMachineContext = <ContextType, EventType extends EventObject>() =>
  createContext<{
    state: StateType<ContextType, EventType>;
    send: SendType<ContextType, EventType>;
  } | null>(null);

Consuming the context

When consuming the React context, I faced another issue: TypeScript indicates that state and send properties are not always defined (because React context value is initially null).

export const ModuleLayoutContext = createMachineContext<ContextType, EventType>();

const SomeHighUpComponent: React.FC<{}> = () => {
  const [state, send] = useMachine<ContextType, EventType>(moduleLayoutMachine);
  ...
  return(
    <ModuleLayoutContext.Provider value={{ state, send }}>
       ...
    </ModuleLayoutContext>
  )
}

const ConsumerComponent = () => {
    const { state, send } = useContext({ machine: ModuleLayoutContext });
    /* state and send are typed as possibly undefined by TypeScript even though
        we initialized their value. */

This implies that I have to check for nullity every single time, even though I know I initialized the state machine higher up in the component tree with a useMachine hook. This is the utils I wrote:

import { useContext } from 'react';
import { EventObject, Interpreter, State } from 'xstate';

export type StateType<ContextType, EventType extends EventObject> = State<
  ContextType,
  EventType,
  any
>;
export type SendType<ContextType, EventType extends EventObject> = Interpreter<
  ContextType,
  any,
  EventType
>['send'];
export type MachineReactContextType<ContextType, EventType extends EventObject> = React.Context<{
  state: StateType<ContextType, EventType>;
  send: SendType<ContextType, EventType>;
} | null>;

type Props<ContextType, EventType extends EventObject> = {
  machine: MachineReactContextType<ContextType, EventType>;
};

const useMachineContext = <ContextType, EventType extends EventObject>({
  machine,
}: Props<ContextType, EventType>) => {
  const context = useContext(machine);

  if (!context) {
    throw Error('State machine is not initialized');
  }

  return { state: context.state, send: context.send };
};

export default useMachineContext;

Because the hook throws an error when context is not defined, TypeScript can narrow the state and send type from SomethingType || undefined to SomethingType || never which is equivalent to SomethingType

Thanks for reading !

documentation enhancement typescript

All 4 comments

The createContext(null) problem is an existing "issue" with React TypeScript (that won't be fixed). Here is a workaround: https://gist.github.com/sw-yx/f18fe6dd4c43fddb3a4971e80114a052

Or you can do createContext(null as any).

@davidkpiano @AleBlondin
I found better solution:

import React from 'react';
import { Interpreter, State } from 'xstate';

export interface RootMachineContext {
  id: string;
}

interface RootMachineToggleEvent {
  type: 'TOGGLE';
  id: string;
}

interface RootMachineScanEvent {
  type: 'SCAN';
}

export type RootMachineEvent =
  | RootMachineToggleEvent
  | RootMachineScanEvent;


export const StateContext = React.createContext<
  [
    State<RootMachineContext, RootMachineEvent>,
    Interpreter<RootMachineContext, any, RootMachineEvent>['send'],
    Interpreter<RootMachineContext, any, RootMachineEvent>,
  ]
>([
  {} as State<RootMachineContext, RootMachineEvent>,
  ((() => {}) as any) as Interpreter<
    RootMachineContext,
    any,
    RootMachineEvent
  >['send'],
  {} as Interpreter<RootMachineContext, any, RootMachineEvent>,
]);

so far the following seems to work well for me when defining types for a machine:

type StateContext = {}
type StateEvent = { type: 'SOME_EVENT' }
type StateSchema = { states: { some_state: {} } }
type TypeState = Distribute<keyof StateSchema["states"], StateContext> // assuming same context

type Context = [
    State<StateContext, StateEvent, StateSchema, TypeState>,
    Interpreter<StateContext, StateSchema, StateEvent, TypeState>['send']
];

type Distribute<U, C> = U extends any ? { value: U; context: C } : never // util


const AppContext = React.createContext<Context>({} as any);

note the addition of TypeState which seems to be needed to make matches() work.

I just happened to find an interim solution for this by using constate.

function usePetFormMachine() {
  return useMachine(petFormMachine)
}

export const [PetFormMachineProvider, usePetFormMachineContext] = constate(
  usePetFormMachine,
)

function SomeComponent() {
  const [current, send] = usePetFormMachineContext()

  return current.value
}

function App() {
  return (
    <PetFormMachineProvider>
      <SomeComponent />
    </PetFormMachineProvider>
  }
}

I know that using a utility library is not what this issue is about though. Just wanted to share in case it could help someone avoid the extensive type ritual while we have a more definite answer :D

Was this page helpful?
0 / 5 - 0 ratings

Related issues

3plusalpha picture 3plusalpha  路  3Comments

kurtmilam picture kurtmilam  路  3Comments

doup picture doup  路  3Comments

bradwoods picture bradwoods  路  3Comments

mattiamanzati picture mattiamanzati  路  3Comments