NEW 我們Astro v5.1的新blog上線啦 »

· Joseph · Frontend  · 3 min read

A powerful react hook - useSyncExternalStore

A powerful react hook - useSyncExternalStore

It is an interesting hook called useSyncExternalStore came from React 18. After I went through the doc, I had totally no idea about how to use it and why it works. Luckily, I got a task I thought I can use this hook, and meanwhile I traced the source code to understand useSyncExternalStore implementation. In this article, I will explain how it works internally and show a demo which is differ from the doc.

TOC

useSyncExternalStore source code

Let us read this helper checkIfSnapshotChanged first. It compares the return value of getSnapshot with prevValue, and return true if they are different.

function checkIfSnapshotChanged<T>(inst: {
  value: T,
  getSnapshot: () => T,
}): boolean {
  const latestGetSnapshot = inst.getSnapshot;
  const prevValue = inst.value;
  try {
    const nextValue = latestGetSnapshot();
    return !is(prevValue, nextValue); // Object.is
  } catch (error) {
    return true;
  }
}

Since we have checkIfSnapshotChanged helper, we can now proceed to useSyncExternalStore.

function useSyncExternalStore<T>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => T,
  getServerSnapshot?: () => T,
): T {
  // Read the current snapshot from the store on every render.
  const value = getSnapshot();
  const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}});

  useLayoutEffect(() => {
    inst.value = value;
    inst.getSnapshot = getSnapshot;
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceUpdate({inst});
    }
  }, [subscribe, value, getSnapshot]);

  useEffect(() => {
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceUpdate({inst});
    }
    const handleStoreChange = () => {
      if (checkIfSnapshotChanged(inst)) {
        // Force a re-render.
        forceUpdate({inst});
      }
    };
    // Subscribe to the store and return a clean-up function.
    return subscribe(handleStoreChange);
  }, [subscribe]);

  return value;
}

On every render, it retrieves a getSnapshot value (Line 32) and sets both value and getSnapshot into state. It’s important to note that they use forceUpdate as the setter name. Subsequently, it forces a re-render by forceUpdate({ inst }) if checkIfSnapshotChanged passed (line 38, 45, and 50).

Continuing further, in the useEffect, it returns a clean-up function by invoking subscribe with a handleStoreChange callback function (Line 56). Therefore, useSyncExternalStore should pass a subscribe function with a type with (callback) => () => {}.

useSyncExternalStore demonstration

MenuCard style

According to the width, I have to generate many half circles on the top of menu card. Thus I use useSyncExternalStore hook, listen resize as subscribe, and calculate the count depends on getBoundingClientRect.

function subscribe(callback) {
    window.addEventListener('resize', callback)
    return () => window.removeEventListener('resize', callback)
}
const DynamicHalfCircle = ({ parentRef }) => {
    const circleData = useSyncExternalStore(
      subscribe,
      () => {
        const { width } = parentRef.current?.getBoundingClientRect() || { width: 0 }
        const count = Math.ceil((width - 7 * 2 - PADDING_Y - CIRCLE_GAP) / (CIRCLE_RADIUS * 2 + CIRCLE_GAP))
        const initLeft = Math.max((width - count * CIRCLE_RADIUS * 2 - (count - 1) * CIRCLE_GAP) / 2, 0)
        return JSON.stringify({
          count: Math.max(count, 0),
          width,
          initLeft,
        })
      },
      () =>
        JSON.stringify({
          count: 0,
          width: 0,
          initLeft: 0,
        }),
    )

    const circleInfo = JSON.parse(circleData || '{}')

    return (
      <CircleWrapper>
        {Array.from({ length: circleInfo?.count || 0 }, (_, i) => i).map((i) => (
          <HalfCircle key={i} style={{ left: (CIRCLE_GAP + CIRCLE_RADIUS * 2) * i + (circleInfo.initLeft ?? 0) }} />
        ))}
      </CircleWrapper>
	)
}

Just try to resize codepen below. {% iframe https://codepen.io/joseph780116/embed/QWojadB?default-tab=html%2Cresult %}

Conclusion

Without useSyncExternalStore, I would need to use useEffect and useState. Now, I just need to use useSyncExternalStore hook and subscribe to changes in getBoundingClientRect when window is resized. It is quite useful, isn’t it?

Related Posts

View All Posts »
I18n in NextJS 13 app dir

I18n in NextJS 13 app dir

I used to implement I18n with next-i18next in Next.js. However, after upgrading Next.js 13, there is no need for next-i18next. Instead, you can directly use i1…

use-device-camera

use-device-camera

在用了Javascript串webcam一年之後,我做了這個的react npm package。