react-nativereact-hooksxstate

Awaiting asynchronous params when using xstate `useInterpret`


I want to enable persistance for react-native application.

Following tutorial on https://garden.bradwoods.io/notes/javascript/state-management/xstate/global-state#rehydratestate I can't use asynchronous code inside xstate's hook useInterpret Original code (which uses localStorage instead of AsyncStorage) doesn't have that issue since localStorage is synchronous.

import AsyncStorage from '@react-native-async-storage/async-storage';
import { createMachine } from 'xstate';
import { createContext } from 'react';
import { InterpreterFrom } from 'xstate';
import { useInterpret } from '@xstate/react';

export const promiseMachine = createMachine({
  id: 'promise',
  initial: 'pending',
  states: {
    pending: {
      on: {
        RESOLVE: { target: 'resolved' },
        REJECT: { target: 'rejected' },
      },
    },
    resolved: {},
    rejected: {},
  },
  tsTypes: {} as import('./useGlobalMachine.typegen').Typegen0,
  schema: {
    events: {} as { type: 'RESOLVE' } | { type: 'REJECT' },
  },
  predictableActionArguments: true,
});


export const GlobalStateContext = createContext({
  promiseService: {} as InterpreterFrom<typeof promiseMachine>,
});

const PERSISTANCE_KEY = 'test_key';

export const GlobalStateProvider = (props) => {
  const rehydrateState = async () => {
    return (
      JSON.parse(await AsyncStorage.getItem(PERSISTANCE_KEY)) ||
      (promiseMachine.initialState as unknown as typeof promiseMachine)
    );
  };

  const promiseService = useInterpret(
    promiseMachine,
    {
      state: await rehydrateState(), // ERROR: 'await' expressions are only allowed within async functions and at the top levels of modules.
    },

    (state) => AsyncStorage.setItem(PERSISTANCE_KEY, JSON.stringify(state))
  );

  return (
    <GlobalStateContext.Provider value={{ promiseService }}>
      {props.children}
    </GlobalStateContext.Provider>
  );
};

I tried to use .then syntax to initialize after execution of async function but it caused issue with conditional rendering of hooks.


Solution

  • I had the same use case recently and from what I found there is no native way for xState to handle the async request. What is usually recommended is to introduce a generic wrapper component that takes the state from the AsyncStorage and pass it a prop to where it is needed.

    In your App.tsx you can do something like:

    const [promiseMachineState, setPromiseMachineState] = useState<string | null>(null);
    
    
    useEffect(() => {
      async function getPromiseMachineState() {
        const state = await AsyncStorage.getItem("test_key");
    
        setPromiseMachineState(state);
      }
    
      getAppMachineState();
    }, []);
    
    return (
      promiseMachineState && (
        <AppProvider promiseMachineState={promiseMachineState}>
          ...
        </AppProvider>
      )
    )
    

    And then in your global context you can just consume the passed state:

    export const GlobalStateProvider = (props) => {
      const promiseService = useInterpret(
        promiseMachine,
        {
          state: JSON.parse(props.promiseMachineState)
        },
        (state) => AsyncStorage.setItem(PERSISTANCE_KEY, JSON.stringify(state))
      );
    
      return (
        <GlobalStateContext.Provider value={{ promiseService }}>
          {props.children}
        </GlobalStateContext.Provider>
      );
    };