Persistent global state with xState and Next.js
Tags: webdev, react, nextjs, xstate
Published: 2023-06-20 12:34:24

### TLDR
If you are curious about the full-working example, it is [here](https://github.com/g-todorov/xstate-persistent-state-example).

### Background
I recently started experimenting with Next.js 13 and thought it would be a good idea to explore some basic web application paradigms specific to the framework. One topic that always comes up is global state and its persistence, especially in combination with xState.

There is a great resource on the topic [here](https://garden.bradwoods.io/notes/javascript/state-management/xstate/global-state#persiststate). However, I encountered a few issues while following it, so I decided to document and share my findings.

I used the `npx create-next-app@latest` util to scaffold the application. 

### createActorContext

Recently, xState introduced a useful utility for integrating state machines with React context called `createActorContext`. Its implementation is straightforward. The `createActorContext(machine)` method takes a machine argument and returns an object that contains a Provider.

```typescript
export const appMachine = createMachine({
  id: "app",
  schema: {
    events: {} as { type: "GO_TO_STATE_1" } | { type: "GO_TO_STATE_2" },
  },
  on: {
    GO_TO_STATE_1: { target: `state1`, internal: false },
    GO_TO_STATE_2: { target: `state2`, internal: false },
  },
  states: {
    state1: {},
    state2: {},
  },
});

export const AppContext = createActorContext(appMachine);
```
This creates a globally accessible machine that allows us to switch between different states.

As mentioned in the Next.js documentation, we cannot directly use the `AppContext.Provider` in the root layout without changing it to a client component with the `use client` directive. To address this, we can add the directive to the file containing our `AppContext` and prepare a wrapper for the provider that will be used in the `RootLayout`.

```typescript
"use client";

/* ... */

export function AppProvider({ children }: PropsWithChildren<{}>) {
  return <AppContext.Provider>{children}</AppContext.Provider>;
}
```

```typescript
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <AppProvider>{children}</AppProvider>
      </body>
    </html>
  );
}
```

Now we can consume our context from any page component and take advantage of the `useActorRef` and `useSelector` hooks provided by the `createActorContext` method. We can use the actor reference to `send` events to the `appMachine`, and with the `useSelector` hook, we can interpret the machine and reduce the number of re-renders if needed.

```typescript
"use client";

import { AppContext } from "@/contexts/app";
import styles from "./page.module.css";

export default function Home() {
  const actor = AppContext.useActorRef();
  const stateValue = AppContext.useSelector((state) => state.value);

  return (
    <main className={styles.main}>
      <button
        className={`${styles.button} ${
          stateValue === "state2" ? styles.selected : ""
        }`}
        onClick={() => {
          actor.send({ type: "GO_TO_STATE_2" });
        }}
      >
        STATE 2
      </button>
      <button
        className={`${styles.button} ${
          stateValue === "state1" ? styles.selected : ""
        }`}
        onClick={() => {
          actor.send({ type: "GO_TO_STATE_1" });
        }}
      >
        STATE 1
      </button>
    </main>
  );
}
```

Currently, the button border of the selected state is painted in red, but we lose this styling after the browser is refreshed.

### Persist and rehydrate

Before rehydrating our state, we need to store it somewhere. Since we're working on the web, the `localStorage` interface is a perfect choice.

To ensure that we always have the latest state in storage, we can use the third argument of the `createActorContext` method, which is an observer that returns the newest state of the machine on every change.

```typescript
export const AppContext = createActorContext(appMachine, {}, (state) => {
  localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(state));
});
```

Now we can pass the rehydrated state to our context. The `AppContext.Provider` has an `options` prop, and for our convenience, the type of the `state` property in the `options` object is the same as the one we stored.

You can read more about why the `typeof window !== "undefined"` condition is needed [here](https://developer.school/snippets/react/localstorage-is-not-defined-nextjs)

```typescript
function getPersistentAppState() {
  if (typeof window !== "undefined") {
    return (
      JSON.parse(localStorage?.getItem(LOCAL_STORAGE_KEY) ?? "false") ||
      appMachine.initialState
    );
  }
}

export function AppProvider({ children }: PropsWithChildren<{}>) {
  return (
    <AppContext.Provider options={{ state: getPersistentAppState() }}>
      {children}
    </AppContext.Provider>
  );
}
```
Finally, after selecting your desired state, you can be sure that it won't be lost.

### Conclusion

I still can't fully envision xState's place in the SSR-first world, but it has once again proven itself as one of the best framework-agnostic libraries.