How to reflect hook state across islands 🏝
Tags: javascript, webdev, react, tutorial
Published: 2022-07-22 10:02:00

["Island" architecture](https://jasonformat.com/islands-architecture) is a term coined relatively recently that describes frontends composed of multiple entrypoints. It challenges the traditional approach of rendering a giant tree of components, enabling more clear isolation of dynamic, hydratable elements from static content. And it's baked into [Fresh](https://fresh.deno.dev/), a new framework for [Deno](https://deno.land/) which I'm currently using for a project (coming soon)! 

But with this isolation comes limitations that inhibit regular patterns, like shared state. In this walkthrough I'll cover how I managed to synchronize hook state across different islands and keep my application's logic organized.

## The hook in question ☀️/🌙

To enable a dark mode preference in my project I added this simple hook to interface with `"prefers-color-scheme: dark"`, adding a `dark` class to the `body` element and setting this in `localstorage` to persist any preference override:

```jsx
export function useDarkMode() {
    const [dark, setDark] = useState(false);
  
    function toggleDarkMode() {
      const prefersDark = document.body.classList.toggle('dark');
      setDark(prefersDark);
      localStorage.setItem('prefers-dark', prefersDark);
    }
  
    useEffect(() => {
      const prefersDark = localStorage.getItem('prefers-dark') === 'true';
      const devicePrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  
      if ((prefersDark === null || prefersDark) && devicePrefersDark) {
        toggleDarkMode();
      }
    }, []);
  
    return [dark, toggleDarkMode];
}
```

This works for the render tree that has the button that triggers `toggleDarkMode`, but due to the island approach this render tree (and the state within) is completely isolated from others. To ensure all elements are in the correct `dark` state regardless of entrypoint there needs to be a lifeline between the islands.

## Enter: event dispatching 🛟

While there are many approaches to solving this issue (`MutationObserver`, etc), the simplest is to dispatch an event that other instances of this hook can listen for.

In this case, this triggers each island to call the `toggleDarkMode` function and (with the proper conditions) keep its state in sync with the triggering instance of the hook. Here are the modifications needed for the above hook to achieve this:

```jsx
export function useDarkMode() {
    function toggleDarkMode() {
        // same code as above
        window.dispatchEvent(new Event('dark-mode-preference-updated'));
    }

    function respondToEvent() {
        const prefersDark = document.body.classList.contains('dark');
        setDark(prefersDark);
    }

    useEffect(() => {
        const prefersDark = localStorage.getItem('prefers-dark') === 'true';
        const devicePrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

        if ((prefersDark === null || prefersDark) && devicePrefersDark) {
            if (!document.body.classList.contains('dark')) {
                toggleDarkMode();
            } else if (!dark) {
                setDark(true);
            }
        }

        window.addEventListener('dark-mode-preference-updated', respondToEvent);

        return () => {
            window.removeEventListener('dark-mode-preference-updated');
        };
    }, []);
}
```

To summarize: each hook instance will, upon mount, check for a user's color scheme preference and set that value in state by calling the same function.

Then, any calls to `toggleDarkMode` will fire an event for every other instance of the hook to receive, which causes each to check the value on `body` and store that in state without performing any mutations.

The `localstorage` value is only set by the triggering hook so subsequent pageloads will get the correct preference value.

---

While this may contradict some of what hooks aim to simplify regarding shared state, it allows logic shared across components to live in a single place. The implementation for this hook is simplified by `document.body.classList` being the source of truth, but more complex events can be used to keep data in sync across instances. Anyways, let me know what you think & if you have other suggestions for mirroring state across different entrypoints!