React Native with XState in Global Context causes entire App to reload

182 views Asked by At

I have a React Native Expo App and I am using XState to keep a global state machine that is passed down the component tree using the Context API.

App.js: (only relevant parts shown - e.g. smSend function here is passed in context.

const GlobalXStateMachineContext = createContext({});
 
//Here stateDef and actionDef are state machine and actions
const stateMachine = createMachine(stateDef, actionDef);

export default function App() {

  console.log("APP INVOKED!!");

  const [smState, smSend] = useMachine(stateMachine);
  return (
    <Provider store={store}>
      <GlobalXStateMachineContext.Provider value={{ smSend }}>
        <NavigationContainer>
          <RootScreen />
        </NavigationContainer>
      </GlobalXStateMachineContext.Provider>
    </Provider>
  );
}
export { GlobalXStateMachineContext };

And then somewhere from deep inside the component tree I am getting a hold of smSend from the context and calling it.

Note that everything is working as expected. But whenever the smSend() is called from anywhere in the component tree the App() is called leading to a complete reload of the entire tree!

Notice the console.log("APP INVOKED") is called everytime smSend is called like so.

NestedFunction.js (again only relevant code)

import { GlobalXStateMachineContext } from './App';

const NestedFunction = () => {
  const smSend = useContext(GlobalXStateMachineContext).smSend;
  ....
  smSend({ payload });
}

I am not sure if this is the expected behavior of Context API or XState or a quirk using the two. This is resulting in performance degradation and may lead to hard to find bug.

Is there anyway this full reload of the App may be avoided?

2

There are 2 answers

0
rnk On BEST ANSWER

I was able to resolve this issue. This was a documented behavior in XState/react https://xstate.js.org/docs/recipes/react.html#improving-performance

Instead of the useMachine hook, I switched to useInterpret as described in the docs above.

import { useInterpret } from '@xstate/react';
const scoreService = useInterpret(stateMachine);

...
return (
<Provider store={store}>
  <GlobalXStateMachineContext.Provider value={{ scoreService }}>
    <RootScreen />
  </GlobalXStateMachineContext.Provider>
</Provider>
);
...
export { GlobalXStateMachineContext };

And NestedFunction.js (again only relevant code)

const { send } = useContext(GlobalXStateMachineContext).scoreService;
....
send({ payload });
2
fengelhardt On

Try wrapping your components such as RootScreen in React.memo this will prevent those components from re-rendering unless needed. React memo docs

FYI React.memo should not wrap components that are meant to re-render a bunch.

Also if you are returning inline components inside a hook component, those inline components should be either removed or memorized with the React.useMemo hook.