The Ultimate Guide to React Suspense

0

React developers may have heard of Suspense at least once. Introduced in React v16.6.0, this feature helps efficiently manage data loading states. However, many developers still struggle to fully utilize Suspense. In this article, I will explain the concept of Suspense and how to effectively use it in your projects.

What is Suspense?

Suspense is one of React’s concurrent rendering features that manages loading states while a component processes asynchronous tasks. It improves user experience and enhances code readability. For example, you can display a spinner while fetching data or show an error message if an error occurs.

Building a Library with Suspense Support

Although the official API documentation provided by the React team is still incomplete, we can build our own Suspense-supporting library. This will deepen your understanding of how Suspense works and how to apply it in real projects.

1. Implementing the FetchCache Class

First, create a FetchCache class to manage data requests.

class FetchCache {
  requestMap = new Map();
  subscribers = new Set();

  fetchUrl(url, refetch) {
    const currentData = this.requestMap.get(url);

    if (currentData) {
      if (currentData.status === "pending") return currentData;
      if (!refetch) return currentData;
    }

    const pendingState = { status: "pending" };
    this.requestMap.set(url, pendingState);

    const broadcastUpdate = () => {
      setTimeout(() => {
        for (const callback of this.subscribers) {
          callback();
        }
      }, 0);
    };

    fetch(url)
      .then((res) => res.json())
      .then((data) => {
        this.requestMap.set(url, { status: "fulfilled", value: data });
      })
      .catch((error) => {
        this.requestMap.set(url, { status: "rejected", reason: error });
      })
      .finally(() => {
        broadcastUpdate();
      });

    broadcastUpdate();
    return pendingState;
  }

  subscribe(callback) {
    this.subscribers.add(callback);
    return () => this.subscribers.delete(callback);
  }
}

2. Implementing the FetchCacheProvider Component

Next, create a FetchCacheProvider to provide a FetchCache instance throughout the React tree.

const fetchCacheContext = createContext(null);

const FetchCacheProvider = ({ children }) => {
  const [fetchCache] = useState(() => new FetchCache());

  useEffect(() => {
    const rerender = () => setEmptyState({});
    const unsubscribe = fetchCache.subscribe(rerender);
    return unsubscribe;
  }, [fetchCache]);

  return (
    <fetchCacheContext.Provider value={{ fetchUrl: fetchCache.fetchUrl.bind(fetchCache) }}>
      {children}
    </fetchCacheContext.Provider>
  );
};

3. Implementing the useFetch Hook

Now, create the useFetch hook to write logic that fetches data using FetchCache.

const useFetch = (url) => {
  const { fetchUrl } = useContext(fetchCacheContext);
  const state = fetchUrl(url);

  const isPending = state.status === "pending";
  if (isPending) throw state;

  const error = state.reason;
  if (error) throw error;

  const data = state.value;

  return [data, () => fetchUrl(url, true)];
};

Now, let’s create an actual component using the features we implemented above.

const User = ({ userId }) => {
  const [data, reload] = useFetch(`https://jsonplaceholder.typicode.com/users/${userId}`);

  return (
    <div>
      <h2>{data.name}</h2>
      <p>{data.email}</p>
      <button onClick={reload}>Reload</button>
    </div>
  );
};

const App = () => {
  return (
    <FetchCacheProvider>
      <ErrorBoundary fallback={<div>Error occurred</div>}>
        <Suspense fallback={<div>Loading...</div>}>
          <User userId={1} />
        </Suspense>
      </ErrorBoundary>
    </FetchCacheProvider>
  );
};

Conclusion

By leveraging Suspense, you can easily manage complex data loading states. This enhances user experience and improves code readability. Now, try building your own Suspense-supporting library and apply it to your projects.

References

Leave a Reply