Select Page

useSyncExternalStore React API

by | Mar 18, 2024

You might have heard about a new tool called useSyncExternalStore() in React 18. It helps connect your React app to outside data sources. Usually, it’s used by fancy internal tools like Redux to manage state.

The official documentation explains that useSyncExternalStore is a helpful hook for accessing data from external sources in a way that works well with advanced rendering features in React. This hook returns the value from the external source and requires three functions:

  1. subscribe: This function registers a callback that gets called whenever there’s a change in the external data source.
  2. getSnapshot: This function retrieves the current value from the external data source.
  3. getServerSnapshot: This optional function returns a snapshot of the data used during server rendering.

Here’s an example provided in the beta documentation:

function subscribe(callback) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine,
    () => true
  );
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

This example demonstrates how to use useSyncExternalStore to check if the user is online or offline.

Interestingly, browser history can also be treated as an external data source. Let’s explore how to use useSyncExternalStore with React-Router.

But how can you use useSyncExternalStore() in your own code?

Let’s talk about a problem first: sometimes React hooks give back more data than we actually need, causing unnecessary re-renders. For example, take useLocation() from React-Router. It gives us a lot of info like the current path and hash. But if we don’t use all of it, our app might re-render for no reason.

Over-returning hooks

Consider this scenario: you have a component that only shows the current path, but it’s still listening to changes in the hash. So, when you click on a link with a hash, it re-renders even though it doesn’t care about the hash.

To fix this, we can use useSyncExternalStore(). It helps us subscribe to external data without causing unnecessary re-renders.

Now, let’s see how we can use it with React-Router to fix our previous problem.

We can create a custom hook called useHistorySelector():

function useHistorySelector(selector) {
  const history = useHistory();
  return useSyncExternalStore(history.listen, () =>
    selector(history)
  );
}

Now, instead of using useLocation(), we can use useHistorySelector() like this:

function CurrentPathname() {
  const pathname = useHistorySelector(
    (history) => history.location.pathname
  );
  return <div>{pathname}</div>;
}

function CurrentHash() {
  const hash = useHistorySelector(
    (history) => history.location.hash
  );
  return <div>{hash}</div>;
}

This way, our components will only re-render when the data they actually use changes.

If a hook gives you data you don’t show, it might make your React app re-render unnecessarily. So, if you use something like useLocation() but don’t actually need all the data it provides, it could slow down your app without you realizing.

The aim isn’t to complain about React-Router. Instead, it’s to show the issue. We’re using useLocation() as an example because it’s a common one. But keep in mind, your own React hooks and other libraries could have the same problem of giving back too much data.

Scroll re rendering

Lastly, there are many other external data sources you can subscribe to using useSyncExternalStore(). For example, you can track the scroll position of a page. This can help optimize your app’s performance by reducing unnecessary re-renders.

Here’s another example: tracking the vertical scroll position of a page. We can create a custom React hook for this purpose:

// This memoized constant function helps prevent unnecessary subscribe/unsubscribe
// In practice, it's not a big deal
function subscribe(onStoreChange) {
  global.window?.addEventListener("scroll", onStoreChange);
  return () =>
    global.window?.removeEventListener(
      "scroll",
      onStoreChange
    );
}

function useScrollY(selector = (id) => id) {
  return useSyncExternalStore(
    subscribe,
    () => selector(global.window?.scrollY),
    () => undefined
  );
}

With this hook, we can get the vertical scroll position of the page. Additionally, we can provide an optional selector function to manipulate the value returned by the hook.

Let’s create some components to use this hook:

function ScrollY() {
  const scrollY = useScrollY();
  return <div>{scrollY}</div>;
}

function ScrollYFloored() {
  const to = 100;
  const scrollYFloored = useScrollY((y) =>
    y ? Math.floor(y / to) * to : undefined
  );
  return <div>{scrollYFloored}</div>;
}

The ScrollY component simply displays the current vertical scroll position of the page, while the ScrollYFloored component rounds it down to the nearest multiple of 100.

Conclusion

In conclusion, useSyncExternalStore() is a handy tool that can help improve the performance of your React app by preventing unnecessary re-renders. If you haven’t upgraded to React 18 yet, don’t worry. There’s a workaround you can use until then.

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

Looking For Something?

Follow Us

Related Articles

How to Create Custom Hooks in React: Simplified Guide with Examples

How to Create Custom Hooks in React: Simplified Guide with Examples

Heard about custom hooks but not sure how to harness their power? In this blog post, we’ll delve into the wonderful world of custom hooks in React, breaking down what they are, how to create them, and why they’re a game-changer for your projects. What Are Custom...

Subscribe To Our Newsletter

Subscribe To Our Newsletter

Join our mailing list to receive the latest news and updates from our team.

You have Successfully Subscribed!