Table of Contents
Global State in Next.js using XState
This note details how to create a global state in a Next.js project using XState. We will create 1 finite-state machine that will control the global state. The machine will be wrapped in a React context. Accessible to all components without the need for prop drilling. A UI component will be added that will read & write to the global state. Lastly, a 2nd UI component will be added. Optimized to prevent unnecessary renders. The file structure will be:
my-next-app ├── pages│ ├── _app.ts│ └── index.tsx├── src│ ├── app.css│ ├── ComponentA.tsx│ ├── ComponentB.tsx│ ├── contexts.ts│ └── myMachine.ts...
1. Create a Machine
Our 1st step will be to create a machine. It will have 3 states;
state3 & 3 events;
Each event will change the machine's state:
2. Create a React Context
To make the machine accessible in any React component without the need for prop drilling, we will use React's Context API.
We'll create a React context with a value of an object.
Then we'll add a property
myService to this object.
Think of a service as a machine that has been started.
So when we start our machine, we'll store in our context under the
We are using an object here so in future, if we have more services we want to add to the global state, we can easily add another.
3. Start the Machine
In Next.js, the component exported from the
pages/_app.tsx is persisted when the user navigates to different pages.
This is where we want to start our machine.
XState's React packages provides different 2 hooks to start a machine:
useMachine: starts a machine & returns the machine. This will cause a component to re-render every time the machine changes.
useInterpret: starts a machine & returns a service (a static reference) of the machine. Being static, its value never changes, so it will not cause re-renders. This can be subscribed to & is what we will do within other components that we do want to re-render when the machine changes.
App component is a parent to all other components in a Next.js app, every time it re-renders, the whole app would re-render.
We want to minimize re-renders of this component, so we'll use the
MyContext's provider & pass in the service.
Our machine is now running and its service can now be subscribed to within any component in our app.
4. Access Global State
Now lets create a React component & access the global state without prop drilling.
MyContext into the useContext hook to get access to the global state.
useActor hook can be passed
myService to subscribe to it.
The component can now read the machine's state & send events to it.
The component we just made has a performance issue. It will re-render when anything changes in our machine. If, for example, our component was only interested in knowing the machine was in state3 or not. It would be unnesscarily re-rendering when the machine changes from state1 to state2 & vice versa. To prevent this, we can use selectors. A selector is a function that returns specific information from the machine, such as is the machine in state3 or not?. Lets create this selector.
Now lets make a new component,
ComponentB that won't have unnecessary re-renders.
To do this, we'll need to replace the useActor hook with
useSelector and pass in our selector.
6. Persist State
A common requirement is to persist & rehydrate global state (save it so when a user reopens your app, they land where they left off).
The 3rd argument of
useInterpret accepts a function.
An observer that will fire whenever the state changes.
It passes the state of the machine which we can store in local storage.
7. Rehydrate State
The 2nd argument of
useInterpret is an options object.
We can pass it our persisted state.
The machine will now start in this state, rather than its initial state.
We now have our optimized component. A sandbox has been created of our 2 components connected to a global state. Unfortunatley, I wasn't able to get the sandbox working with local storage, so persisting & rehydrating isn't included.