State transitions

For a feature to transition from one state to another a dispatch has to happen. A dispatch can either be an action from within the feature, from another component or from the environment.


/*
  Actions triggered within the feature
*/
export type PublicAction = {
  type: 'SIGN_IN'
}

/*
  The actions that can be triggered from outside the
  feature
*/
export type PrivateAction = {
  type: 'ERROR_TIMEOUT'
}

Actions has a type property which describes something that happened in the application. This could be a direct user interaction, a consequence of resolving an environment effect or something else.

Ensuring valid transitions

You want to be exact about a transition, which is why we always add the return type for every possible action inside the reducer. Without this return type, TypeScript will not yell when you add unnecessary properties, which can lead to bugs, especially when spreading existing state. The createReducer factory is where the transitions are expressed.

import { AuthAction } from '../environment/auth'
import { createReducer, States, StatesTransition } from 'react-states'

export type State = 
  | {
    state: 'SIGNED_OUT'  
  }
  | {
    state: 'SIGNING_IN'
  }
  | {
    state: 'SIGNED_IN'
  }
  | {
    state: 'ERROR'
  }
  
export type PublicAction = {
  type: 'SIGN_IN'
}

export type Feature = States<State, PublicAction>

export type Transition = StatesTransition<Feature>

const reducer = createReducer<Feature>({
  SIGNED_OUT: {
    SIGN_IN: (): Transition => ({ state: 'SIGNING_IN' })
  },
  SIGNING_IN: {
    SIGN_IN_SUCCESS: ({ user }): Transition => ({ state: 'SIGNED_IN', user }),
    SIGN_IN_ERROR: ({ error }): Transition => ({ state: 'ERROR', error }) 
  },
  SIGNED_IN: {},
  ERROR: {}
})

export const FeatureProvider = () => {
  const feature = useReducer(reducer, { state: 'SIGNING_IN' })
}

You have to define all the states and optionally what actions they respond to. You can define the same action in multiple states, even cause a different transition based on the current state. The transition handlers receives two arguments:

SIGNED_OUT: {
  SIGN_IN: (action, state): Transition => ({ state: 'SIGNING_IN' })
},

The first argument is the action, in this case the SIGN_IN action. The second argument is the current state, in this case SIGNED_OUT. You can use these two arguments to build the new state.

Command

Even though most effects can be expressed as entering a new state, there are many scenarios where you want to run an effect within the same state or conditionally when entering a new state. You can use Commands for this purpose.

import React, { createContext, useReducer } from 'react'
import { createReducer, States, StatesTransition, useCommandEffect } from 'react-states'
import { ApiAction } from '../environment/api'

type State =
  | {
    state: 'LOADING'
  }
  | {
    state: 'LOADED'
    items: Item[]
  }
  
type Command = {
  cmd: 'SAVE_ITEM'
  item: Item
}

type PublicAction = {
  type: 'ADD_ITEM'
  item: Item
}

type Feature = States<State, PublicAction, Command>

type Transition = StatesTransition<Feature>

const reducer = createReducer<Feature>({
  LOADING: {
    'API:FETCH_ITEMS_SUCCESS': ({ items }): Transition => ({
      state: 'LOADED',
      items
    })
  },
  LOADED: {
    ADD_ITEM: ({ item }, state): Transition => [{
        ...state,
        items: state.items.concat(item)
      }, {
        cmd: 'SAVE_ITEM',
        item
      }]
  },
})

const FeatureProvider = () => {
  const feature = useReducer(reducer, { state: 'LOADING' })
  const [state, dispatch] = feature
  
  useCommandEffect(state, 'ADD_ITEM', ({ item }) => {
    // Do whatever
  })
  
  ...
}

Last updated

Was this helpful?