DEV Community

Cover image for Developing a React Library for Suspense for Data Fetching in Concurrent Mode
Daishi Kato
Daishi Kato

Posted on • Originally published at blog.axlight.com

Developing a React Library for Suspense for Data Fetching in Concurrent Mode

New experimental react-hooks-fetch

Introduction

We have been waiting for "Suspense for Data Fetching" for a long time. It is now provided as an experimental feature in the experimental channel.

Check out the official docs for details.

  1. Introducing Concurrent Mode
  2. Suspense for Data Fetching
  3. Concurrent UI Patterns
  4. Adopting Concurrent Mode
  5. Concurrent Mode API Reference

They are trying best to explain new mind sets with analogies. That means it's totally different from the usage with traditional React. Yes, it is different and promising.

This post is to explore a usage with Suspense for Data Fetching. Please note that all features are experimental and the current understanding could be wrong in the future.

To get the benefit of Suspense for Data Fetching in Concurrent Mode, we should use the "Render-as-You-Fetch" pattern. This requires to start fetching before rendering. We need to have new mental model because we are so used to fetching in useEffect or componentDidMount.

This post doesn't provide any specific answer to best practices yet, but here's what I did now.

createFetcher

Let's create a "fetcher" that wraps a fetch function. This can be an arbitrary async function that returns a Promise.

const fetcher = createFetcher(async url => (await fetch(url)).json());
Enter fullscreen mode Exit fullscreen mode

This is a general GET fetcher that takes a url as an input and assumes a JSON response. Typically, we'd want to create more specialized fetchers.

A fetcher provides two methods: prefetch and lazyFetch.

If you invoke prefetch, it will start the fetch function and you will get a "suspendable." A "suspendable" is an object with two properties: data and refetch. The data is to get the promise result, but it will throw a promise if the promise is not resolved. The refetch will run the fetch function again and returns a new "suspendable."

If you invoke lazyFeth, you will get a "suspendable"-like, with fallback data and a lazy flag. It will actually never suspend, but you can treat it as a "suspendable" just like the one returned by prefetch.

Now, the TypeScript typing of createFetcher is the following:

type Suspendable<Data, Input> = {
  data: Data;
  refetch: (input: Input) => Suspendable<Data, Input>;
  lazy?: boolean;
};

type Fetcher<Data, Input> = {
  prefetch: (input: Input) => Suspendable<Data, Input>;
  lazyFetch: (fallbackData: Data) => Suspendable<Data, Input>;
};

export const createFetcher: <Data, Input>(
  fetchFunc: (input: Input) => Promise<Data>,
) => Fetcher<Data, Input>;
Enter fullscreen mode Exit fullscreen mode

The implementation of this is a bit long.

export const createFetcher = (fetchFunc) => {
  const refetch = (input) => {
    const state = { pending: true };
    state.promise = (async () => {
      try {
        state.data = await fetchFunc(input);
      } catch (e) {
        state.error = e;
      } finally {
        state.pending = false;
      }
    })();
    return {
      get data() {
        if (state.pending) throw state.promise;
        if (state.error) throw state.error;
        return state.data;
      },
      get refetch() {
        return refetch;
      },
    };
  };
  return {
    prefetch: input => refetch(input),
    lazyFetch: (fallbackData) => {
      let suspendable = null;
      const fetchOnce = (input) => {
        if (!suspendable) {
          suspendable = refetch(input);
        }
        return suspendable;
      };
      return {
        get data() {
          return suspendable ? suspendable.data : fallbackData;
        },
        get refetch() {
          return suspendable ? suspendable.refetch : fetchOnce;
        },
        get lazy() {
          return !suspendable;
        },
      };
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

The use of prefetch is almost always preferred.
The lazyFetch is only provided as a workaround
for the "Fetch-on-Render" pattern.

Once you get a "suspendable," you can use it in render and React will take care of the rest.

Because we need to invoke prefetch before creating a React element.
we could only do it outside of render functions.
As of writing, we do it in the component file globally,
assuming we know what we want as an initial "suspendable."
This would probably make testing difficult.

useSuspendable

The fetcher created by createFetcher is functionally complete, but it would be nice to have handy hooks to use "suspendable"s.

The simplest one is useSuspendable. It simply stores a single "suspendable" in a local state.

The implementation of useSuspendable is the following.

export const useSuspendable = (suspendable) => {
  const [result, setResult] = useState(suspendable);
  const origFetch = suspendable.refetch;
  return {
    get data() {
      return result.data;
    },
    refetch: useCallback((nextInput) => {
      const nextResult = origFetch(nextInput);
      setResult(nextResult);
    }, [origFetch]),
    lazy: result.lazy,
  };
};
Enter fullscreen mode Exit fullscreen mode

The result returned by the useSuspendable hook is almost like a normal "suspendable" but with a slight difference. If you invoke refetch, instead of returning a new "suspendable," it will replace the state value with the new "suspendable."

The library

I've developed the above code into a library.

https://github.com/dai-shi/react-hooks-fetch

This is highly experimental and it will change.

Demo

Here's one small example using this library.

Codesandbox

There are some other examples in the repo.

Closing notes

I hesitated a bit to write a post like this, which is highly experimental and can even change in a couple of days after writing. Nevertheless, I would like the community to try the new world with Suspense for Data Fetching and give some feedbacks.


Originally published at https://blog.axlight.com on November 3, 2019.

Latest comments (0)