Sync React Component with Global Variable

Sync React Component with Global Variable

·

4 min read

Usually, react component will only read data from states, props or context. Or we can use any third party library such as redux to store global data.This covers most of the cases, but what if we want our component to be sync with a global variable?

For example, we have this globalValue variable in global, and we want to render the variable inside Component. This is fine for the first render, but this global value maybe changed in other places, such as this setTimeout call in below example. In this case, we want to rerender the Component to get the lastest state.

let globalValue = 0;

setTimeout(() => {
  globalValue++;
}, 1000);

export function Component() {
  return <div>demo: {globalValue}</div>;
}

Well, because the globalValue is just a plain variable, it will not trigger react component rerender when it be changed. So we need to let the component know somehow.

The most simple way is to poll the state inside Component. Whenever we detect new value from globalValue, we rerender the component.

import { useEffect, useState } from "react";

let globalValue = 0;

setTimeout(() => {
  globalValue++;
}, 1000);

export function Component() {
  const [localValue, setLocalValue] = useState(globalValue);

  useEffect(() => {
    const id = setInterval(() => {
      if (localValue !== globalValue) {
        setLocalValue(globalValue);
      }
    }, 300);
    return () => {
      clearInterval(id);
    };
  }, [localValue]);

  return <div>demo: {localValue}</div>;
}

This should work, but not efficient, and also pretty ugly.

If we want to get rid of polling, we need to find a way to trigger the rerendering of the component when the global value changes. We can ask ourself, who can trigger the rerendering of the component? Well the simplest answer is to use setState. So we may think of below solution. We create a global callback function, and we set it with value of setState, and we call it when the global value changes.

import { useState } from "react";

let globalValue = 0;
let callback = (v: number) => console.log("dumb callback", v);

setTimeout(() => {
  globalValue++;
  callback(globalValue);
}, 1000);

export function Component() {
  const [localValue, setLocalValue] = useState(globalValue);
  callback = setLocalValue;

  return <div>demo: {localValue}</div>;
}

This should work and seems pretty neat. But what if we want to bind the global value with multiple components? One callback function seems not enough and we need to use an array handle it. And we also need to make sure we will not call the callback when the comonent unmounts.

So let's summarize what we need to do:

  1. Set up useState in each component
  2. Set up a global variable listeners array
  3. Push all setState function into the listeners array
  4. Delete the setState function from the listeners array when component unmount
  5. When the global value changes, call all functions from listeners array, thus triggering components rerendering

Seems a lot if we want to set up a global variable properly sync with react component. Luckiy, react provides a hook call useSyncExternalStore, which just provides all the above functionalities. Let's see how.

import { useSyncExternalStore } from "react";

let globalValue = 0;

let listeners: Array<() => void> = [];
function emitChange() {
  for (const listener of listeners) {
    listener();
  }
}

function subscribe(listener: () => void) {
  // push listener
  listeners = [...listeners, listener];
  return () => {
    // remove listener(will be called when component unmounts)
    listeners = listeners.filter((l) => l !== listener);
  };
}

// return the lastest state
function getSnapshot() {
  return globalValue;
}

setTimeout(() => {
  globalValue++;
  emitChange(); // call listeners when globalValue changes
}, 1000);

export function Component() {
  const localValue = useSyncExternalStore(subscribe, getSnapshot);

  return <div>demo: {localValue}</div>;
}

As you can see, we need to pass 2 params into the hook. The first one is subscribe function, which should do 2 things. One is to push the passed listener into our listeners' array, the other is to return a callback function to remove it. The second param is just to function to return the lastest state we want to use.

When the hook runs, react will pass the listener to our subscribe function. Then we add it into listeners' array. Then we call all listeners when the globalValue changes. Then react will rerender the component because of the function call of the listener. Then react will call getSnapshot function to return the lastest state. And finally, react will call the return callback from subscribe function when component unmounts.

Well, that's all for this article. This hook is pretty powerful, use it when you need it.