Unravelling The Suspense About Render-As-You-Fetch In React

Written by ppiyush13 | Published 2022/06/20
Tech Story Tags: software-development | reactjs | react-suspense | react-hooks | data-fetching | single-responsibility | render-as-you-fetch | react-components

TLDRRender-as-you-fetch has been the talk of the town as the latest data fetching pattern in React. It complements and leverages the Suspense capabilities to provide a smooth developer and user experience. We will create and understand a primitive useFetch hook, that implements this pattern.via the TL;DR App

Suspense was added in React version 16.6.0. It was introduced to declaratively specify the loading UI for asynchronous operations while the user is waiting.

In earlier versions of React, Suspense could be used for code splitting with React.lazy. But the React team envisions Suspense to handle asynchronous operations like loading code, data, images, etc as well.

With the ever-evolving nature of React, now is the right time to hop on this train and learn the basics of data fetching with Suspense. Whether you are at a beginner, intermediate or expert level in React, you will learn something new from this article.

TL;DR

Render-as-you-fetch has been the talk of the town as the latest data fetching pattern in React. It complements and leverages the Suspense capabilities to provide a smooth developer and user experience. We will create and understand a primitive useFetch hook, that implements this pattern.

Check out the demo application and the source code.

Recap Of React Suspense

  • Suspense adds the ability to have a component notify React at render time that it’s waiting for asynchronous data - this is called suspending.
  • The component that needs asynchronous data has to throw a promise. Suspense catches this promise and awaits its resolution. This promise is also referred to as suspender.
  • While waiting, React discards rendering the component and Suspense renders a fallback UI.
  • When the promise is settled, the component is rendered and the fallback UI is removed.

Using Suspense gives us the capability to decide what should be shown until the asynchronous data is loaded. We don't need to do any explicit state management to control the visibility of the fallback UI. The onus of handling these intricacies is with React.

Data Fetching Approaches In React

Since React’s inception, the React team and community have established various data fetching approaches. Each with its own pros and cons. Let’s summarize the major ones:

  • Approach 1: Fetch-on-render

    In this approach, components trigger data fetching in their effects or lifecycle methods. Fetching starts only after the component has been rendered. This approach often leads to “waterfalls”. Read more about this pattern here.

  • Approach 2: Fetch-then-render

    In this approach, first, all the data for the component is fetched. The component is rendered only after data is fetched and provided as a prop to the component. This approach does solve the “waterfall” problems but only to a limited extent. Read more about this pattern here.

  • Approach 3: Render-as-you-fetch

    In this approach, the component that performs data fetching is wrapped in Suspense. A fallback UI prop is provided to Suspense. It is rendered until the data is fetched. To indicate that the fallback UI needs to be shown, the component must throw a promise. Once the promise is settled, React renders the actual component and removes the fallback UI. Read more about this pattern here.

    In this article, we will focus on Approach 3.

Creating The Hook

The interaction between the promise thrown by the component and React Suspense is the heart of the render-as-you-fetch pattern. Let's create a hook to dig deeper.

/* Simple fetcher function that accepts a URL, makes a fetch request and returns data in JSON format */
const fetcher = async <T>(url: string): Promise<T> => {
  const response = await fetch(url);
  return response.json();
};
 
/* Cache to store fetch result, which can be a promise, error or data */
const cache = new Map();
 
/* Suspends the rendering by throwing a promise. Returns the result (data or error) when thrown promise settles */
export const useFetch = <T>(url: string): T => {
  if (cache.has(url)) {
    const { promise, data, error } = cache.get(url);
 
    if (promise) throw promise;
    if (error) throw error;
    else return data;
  }
 
  /** aka suspender */
  const promise = fetcher<T>(url)
    .then((data) => cache.set(url, { data }))
    .catch((error) => cache.set(url, { error }));
 
  cache.set(url, { promise });
  throw promise;
};

The building blocks of the above hook are:

  • fetcher
    • The fetcher function accepts a URL, makes a fetch request and returns data in JSON format.

    • It can be extended further to:

      1. Fetch any type of supported response (blob, text, formData, arrayBuffer).
      2. Provide query params in URL.
      3. Specify the HTTP method.
      4. Set the HTTP headers.
  • cache
    • The cache is an instance of the native JavaScript Map.
    • It stores either the fetcher promise or the fetcher promise result (data/error).
    • Request URLs are treated as the cache keys.
  • useFetch hook
    • The useFetch hook is used to prompt React to show the Suspense fallback until the data is received. It does this by throwing a promise.
    • The result of this Promise will not be used. But React will retry rendering whenever it receives an unsettled promise.
    • This can result in a completed render, an error, or a re-suspension (displaying the fallback).

The interactions between useFetch hook, cache and Suspense are as follows:

  1. The useFetch hook calls the fetcher function with the given URL. It returns a promise.
  2. This promise is cached and thrown.
  3. When a promise is thrown, React suspends the component's rendering and displays the Suspense fallback.
  4. Once the promise is settled, the resolved data or the rejected error is cached. This overrides the previously cached promise (done in step 2).
  5. React attempts to render the hook again. This time hook returns the resolved data or throws the rejected error (cached in step 4).
  6. Now that the data or error is available, React removes the Suspense fallback and displays the actual component or error.

Why Should We Migrate To Data Fetching With Suspense?

  1. Avoids component “waterfalls”.

    Since React can render multiple data fetching components parallelly, there would be no waterfalls.

  2. Early initiation of data fetching.

    This pattern triggers data fetching as soon as the component starts rendering. This saves up on the time needed by the component to receive the data.

  3. Makes code simple to read and understand.

    • React encourages writing declarative code. This works really well for synchronous operations.
    • But while fetching data asynchronously, a spaghetti of states and effects is introduced. It makes the code difficult to read.
    • Suspense for asynchronous operations eliminates the usage of states and effects. Because with Suspense, data is treated like it's available synchronously.
    • Thus, we can write asynchronous code in a synchronous way (like async/await). This makes our code easier to understand.
  4. Single responsibility of data fetching component.

    • In this pattern, the data fetching component or hook is only responsible for fetching data.
    • Showing loading fallback UI (while the user is waiting) is done with Suspense and error handling is done with Error Boundary. This enables responsibility segregation.
  5. React will extend Suspense functionality.

    • The React team is continuously working to provide performance optimizations out of the box.
    • React already has a roadmap in place for releasing advanced features built on top of Suspense. Some examples: SuspenseList, transition with Suspense, etc.
    • Letting React handle loading states with Suspense right now, would allow us to take advantage of future developments later - with minimal code refactoring.

Conclusion

The useFetch hook built above demonstrates the crux of how render-as-you-fetch with Suspense works. This is the core of full-fledged data-fetching libraries like Relay, React Query, SWR, etc. which can be used in production.

The community has accepted Suspense to be a game-changer in the React ecosystem which will keep evolving. So get on this ride and fasten your seat belts. It is going to be an exciting journey ahead!

For a visual understanding of render-as-you-fetch, you can check out the demo application and the source code built using the useFetch hook.

If you find this article useful, please let me know in the comments.

Connect with me

Github

LinkedIn


Featured image by Ant Rozetsky on Unsplash


Written by ppiyush13 | Hi, I am a front-end developer with a passion to untangle the Web.
Published by HackerNoon on 2022/06/20