XState TypeScript - useInterprete Service

1.7k views Asked by At

I am building a login machine with TypeScript and xState combined with React UI.

My machine is:

import { LoginResponse } from 'async/authentication/responseModel';
import Data from 'data';
import { AUTH_TOKEN } from 'Machines/authentication/constants';
import services from 'services';
import { assign, DoneInvokeEvent, Interpreter, Machine, MachineConfig, State } from 'xstate';

export type AutomataContext = {
  authentication: {
    oldToken: {
      exists: boolean;
      token: string;
    };
    token: string;
  };
  login: {
    email: string;
    password: string;
  };
};

export interface AutomataStatesSchema {
  states: {
    SEARCHING_EXISTING_AUTH_TOKEN: {};
    VERIFYING_EXISTING_TOKEN: {};
    LOGIN_VIEW: {};
    AUTHENTICATING: {};
    AUTHENTICATED: {};
    FAILED_AUTH: {};
  };
}

export enum Events {
  ENTER_DATA = 'ENTER_DATA',
  SUBMIT_LOGIN_DATA = 'SUBMIT_LOGIN_DATA',
}

export type SubmitLoginData = {
  type: Events.SUBMIT_LOGIN_DATA;
};

export type EnterData = {
  type: Events.ENTER_DATA;
  value: string;
  field: keyof AutomataContext['login'];
};

export type AutomataEvent = SubmitLoginData | EnterData;
export type AutomataService = Interpreter<AutomataContext, AutomataStatesSchema, AutomataEvent>;

export const enterLoginData = (context: AutomataContext, event: EnterData) => {
  assign({
    login: Object.assign(context.login, { [event.field]: event.value }),
  });
};

export const authenticateUser = (context: AutomataContext, _: any) =>
  services.authenticationService.login(context.login.email, context.login.password);

export const verifyToken = (context: AutomataContext, _: any) =>
  services.authenticationService.validateToken(context.authentication.oldToken.token);

const loginMachineConfig: MachineConfig<AutomataContext, AutomataStatesSchema, AutomataEvent> = {
  id: 'auth-machine',
  initial: 'SEARCHING_EXISTING_AUTH_TOKEN',
  context: {
    authentication: {
      oldToken: {
        exists: false,
        token: '',
      },
      token: '',
    },
    login: {
      email: '',
      password: '',
    },
  },
  on: {
    ENTER_DATA: {
      actions: ['enterLoginData'],
    },
    SUBMIT_LOGIN_DATA: {
      target: 'AUTHENTICATING',
    },
  },
  states: {
    SEARCHING_EXISTING_AUTH_TOKEN: {
      entry: [
        // getTokenFromLocalStorage
        (context, event) => {
          const authentication = Object.assign({}, context.authentication, {
            oldToken: {
              exists: true,
              token: localStorage.getItem(AUTH_TOKEN),
            },
          });
          assign({
            authentication: authentication,
          });
        },
      ],
      after: {
        500: [
          {
            cond: 'doesOldTokenExist',
            target: 'VERIFYING_EXISTING_TOKEN',
          },
          {
            target: 'LOGIN_VIEW',
          },
        ],
      },
      on: {
        ENTER_DATA: undefined,
        SUBMIT_LOGIN_DATA: undefined,
      },
    },
    VERIFYING_EXISTING_TOKEN: {
      invoke: {
        src: 'verifyToken',
        onDone: {
          target: 'AUTHENTICATED',
          actions: [
            // assignExistingTokenToContext
            (context, even) => {
              assign({
                authentication: Object.assign(context.authentication, {
                  token: context.authentication.oldToken.token,
                }),
              });
            },
            //sendLoginDataToStore
            (context, event) => {
              const payload = Data.creators.authentication.saveLoginDataToStore(context.authentication.token);
              Data.store.dispatch(payload);
            },
          ],
        },
        onError: {
          target: 'LOGIN_VIEW',
          actions: [
            // removeExpiredTokenFromStorage
            (context: AutomataContext, event: any) => localStorage.removeItem(AUTH_TOKEN),
          ],
        },
      },
      on: {
        ENTER_DATA: undefined,
        SUBMIT_LOGIN_DATA: undefined,
      },
    },
    LOGIN_VIEW: {},
    AUTHENTICATING: {
      invoke: {
        src: 'authenticateUser',
        onDone: {
          target: 'AUTHENTICATED',
          actions: [
            // clearLoginData
            () =>
              assign({
                login: (context: AutomataContext, event): any =>
                  Object.assign(context.login, { password: '', email: '' }),
              }),
            // assignAuthTokenToContext
            (context: AutomataContext, event: DoneInvokeEvent<LoginResponse>) => {
              assign({
                authentication: Object.assign(context.authentication, {
                  token: event.data.token,
                }),
              });
            },
            // placeAuthTokenToLocalStorage
            (context: AutomataContext, event: any) => localStorage.setItem(AUTH_TOKEN, context.authentication.token),
          ],
        },
      },
      on: {
        ENTER_DATA: undefined,
        SUBMIT_LOGIN_DATA: undefined,
      },
    },
    AUTHENTICATED: {
      on: {
        ENTER_DATA: undefined,
        SUBMIT_LOGIN_DATA: undefined,
      },
    },
    FAILED_AUTH: {},
  },
};

const options: any = {
  services: {
    verifyToken,
    authenticateUser,
  },
  actions: {
    enterLoginData,
  },
  guards: {
    doesOldTokenExist: (context: AutomataContext, _: any) => context.authentication.oldToken.exists === true,
  },
};

export default Machine<AutomataContext, AutomataStatesSchema, AutomataEvent>(loginMachineConfig, options);

In my React component:

index.tsx

import { useInterpret, useMachine } from '@xstate/react';
import machine from 'Machines/authentication/login';
import Form from 'views/routes/authentication/Login/Form';

const Login = () => {

  const loginService = useInterpret(machine);

  return (
    <>
      <Form service={loginService} />
    </>
  );
};

export default Login;

Form.tsx

import { AutomataService } from 'Machines/authentication/login';
import { Link } from 'react-router-dom';

type Props = {
  service: AutomataService;
};

const Form = ({ service }: Props) => {
 //some react code here
}

I am getting a type mismatch error here

Type 'Interpreter<AutomataContext, AutomataStatesSchema, AutomataEvent, any, TypegenDisabled & { missingImplementations: { ...; }; } & AllowAllEvents & { ...; }>' is not assignable to type 'AutomataService'.
  The types of 'machine.getStateNodes' are incompatible between these types.
    Type '(state: StateValue | State<AutomataContext, AutomataEvent, any, any, TypegenDisabled & { missingImplementations: { ...; }; } & AllowAllEvents & { ...; }>) => StateNode<...>[]' is not assignable to type '(state: StateValue | State<AutomataContext, AutomataEvent, any, { value: any; context: AutomataContext; }, TypegenDisabled>) => StateNode<...>[]'.
      Types of parameters 'state' and 'state' are incompatible.
        Type 'StateValue | State<AutomataContext, AutomataEvent, any, { value: any; context: AutomataContext; }, TypegenDisabled>' is not assignable to type 'StateValue | State<AutomataContext, AutomataEvent, any, any, TypegenDisabled & { missingImplementations: { ...; }; } & AllowAllEvents & { ...; }>'.
          Type 'State<AutomataContext, AutomataEvent, any, { value: any; context: AutomataContext; }, TypegenDisabled>' is not assignable to type 'StateValue | State<AutomataContext, AutomataEvent, any, any, TypegenDisabled & { missingImplementations: { ...; }; } & AllowAllEvents & { ...; }>'.
            Type 'State<AutomataContext, AutomataEvent, any, { value: any; context: AutomataContext; }, TypegenDisabled>' is not assignable to type 'State<AutomataContext, AutomataEvent, any, any, TypegenDisabled & { missingImplementations: { actions: never; delays: never; guards: never; services: never; }; } & AllowAllEvents & { ...; }>'.
              Types of property 'machine' are incompatible.
                Type 'StateMachine<AutomataContext, any, AutomataEvent, { value: any; context: AutomataContext; }, BaseActionObject, any, TypegenDisabled> | undefined' is not assignable to type 'StateMachine<AutomataContext, any, AutomataEvent, any, BaseActionObject, any, TypegenDisabled & { missingImplementations: { ...; }; } & AllowAllEvents & { ...; }> | undefined'.
                  Type 'StateMachine<AutomataContext, any, AutomataEvent, { value: any; context: AutomataContext; }, BaseActionObject, any, TypegenDisabled>' is not assignable to type 'StateMachine<AutomataContext, any, AutomataEvent, any, BaseActionObject, any, TypegenDisabled & { missingImplementations: { ...; }; } & AllowAllEvents & { ...; }>'.
                    Types of property '__TResolvedTypesMeta' are incompatible.
                      Type 'TypegenDisabled' is not assignable to type 'TypegenDisabled & { missingImplementations: { actions: never; delays: never; guards: never; services: never; }; } & AllowAllEvents & { indexedActions: IndexByType<...>; indexedEvents: Record<...> & { ...; }; invokeSrcNameMap: Record<...>; }'

I am lost here, what this TypeScript error is really trying to say. Is there something wrong with the TypeSystem itself? or Have I built the interpreter wrong?

1

There are 1 answers

0
parker_codes On

The way I've always done it is InterpreterFrom<typeof myMachine>. There are also type issues with the Machine constructor, I think. The official recommended way is createMachine().