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.
- Introducing Concurrent Mode
- Suspense for Data Fetching
- Concurrent UI Patterns
- Adopting Concurrent Mode
- 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());
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>;
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;
},
};
},
};
};
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,
};
};
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.
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.