Originally published at https://sergiodxa.com/articles/render-as-you-fetch/
Render as you Fetch is a pattern that lets you start fetching the data you will need at the same time you start rendering the component using that data. This way you don't need to wait to render in the loading state to start fetching, called Fetch on Render, neither wait for fetching to finish to start rendering, called Fetch Then Render.
Let's build an example app using all of those patterns to understand how they all work.
Fetch on Render
This is the most common pattern of the three, the idea here is that you initially render your component with a loading state and then you start fetching the data.
// fetcher.js
const sleep = ms => new Promise(r => setTimeout(r, ms));
export default function fetcher(url) {
return sleep(1000).then(() => ({ url }));
}
First, we create a simple fetcher function, this one will sleep for one second and then return with an object containing the received URL.
import React from "react";
export default function Loading() {
return <p>Loading...</p>;
}
Then let's build a simple Loading
component with a message. It will be used as a fallback for both the data fetching and the lazy loading.
// resource.js
import React from "react";
import useSWR from "swr";
import fetcher from "./fetcher";
import Loading from "./loading";
export default function Resource({ id }) {
const { data } = useSWR(`/api/resource/${id}`, fetcher);
if (!data) {
return <Loading />;
}
return <p>{data.url}</p>;
}
Now let's build our Resource
component, this one will call SWR with the URL appending the props.id
and using our fetcher, inside it we will check if data
is not defined and render our Loading
component, if it's defined we will render the URL.
Here SWR will call our fetcher
function passing the URL after the component rendered one time, using an effect to call our function.
// app.js
import React from "react";
const sleep = ms => new Promise(r => setTimeout(r, ms));
const LazyResource = React.lazy(() =>
sleep(1000).then(() => import("./resource"))
);
export default function App() {
const [id, setID] = React.useState(null);
function handleChange(event) {
setID(event.target.value);
}
return (
<>
<label htmlFor="id">Resource ID:</label>{" "}
<input id="id" type="text" onChange={handleChange} value={id} />
{id && (
<React.Suspense fallback={<p>Loading...</p>}>
<LazyResource id={id} />
</React.Suspense>
)}
</>
);
}
Now our App
component will render a simple input where you could write an ID, then it will update a state to store the ID, if the ID is not falsy we will render our Resource
component, however, we are importing our component using React.lazy
to lazy load it, this means if you never change the ID then you will never load the code for that component, but that also means we need to first load the component, which in our case takes at least one second due our sleep function, and then render and then trigger the fetcher function.
Let's see this example running in CodeSandbox.
If we try it our application is now taking two seconds to show the URL the first time and one second for every change after that.
It works, but it's not ideal.
Fetch Then Render
The Fetch Then Render approach goes in a different direction, instead of rendering and then starting the fetch we will fetch the data and then render after it fetched. While it sounds similar it has a different implementation.
Most of our code will remain the same, let's focus on the changes.
// resource.js
import React from "react";
export default function Resource({ data }) {
return <p>{data.url}</p>;
}
In our Resource
component we are not handling our loading state anymore, neither we are fetching the data, instead, we are receiving the data from the parent component.
// app.js
import React from "react";
import useSWR from "swr";
import Loading from "./loading";
import fetcher from "./fetcher";
const sleep = ms => new Promise(r => setTimeout(r, ms));
const LazyResource = React.lazy(() =>
sleep(1000).then(() => import("./resource"))
);
export default function App() {
const [id, setID] = React.useState(null);
const { data } = useSWR("/api/resource/" + id, fetcher);
async function handleChange(event) {
setID(event.target.value);
}
return (
<>
<label htmlFor="id">Resource ID:</label>{" "}
<input id="id" type="text" onChange={handleChange} value={id} />
{!id ? (
<p>Enter ID</p>
) : data ? (
<React.Suspense fallback={<Loading />}>
<LazyResource data={data} />
</React.Suspense>
) : (
<Loading />
)}
</>
);
}
In our App
component we are now updating the ID and then letting SWR trigger a new call of fetcher, basically, we moved the data fetching from the component using the data to the parent component. In the return
statement of our component, we now check if we have a valid ID and then if we have data to know if we should render the Loading
component.
Let's see it running again in CodeSandbox.
It still takes two seconds to render the Resource
component the first time we write an ID. It wasn't an improvement compared to the Fetch on Render pattern, just a different way to do it.
Render as you Fetch
Now let's see the pattern we are more interested in, Render as you Fetch, the idea here is that you, as a developer, most of the time, know what data your component needs, or there is a way to know it. So instead of waiting for the fetch to finish to render or the render to finish to fetch we could render and fetch at the same time.
Let's see it implemented. First, we need to update our Resource
component.
// resource.js
import React from "react";
import useSWR from "swr";
import fetcher from "./fetcher";
export default function Resource({ id }) {
const { data } = useSWR(`/api/resource/${id}`, fetcher, { suspense: true });
return <p>{data.url}</p>;
}
Note that we added the data fetching back into the component, however, we are not handling the loading state, instead, we are configuring SWR to suspend our component until the data is fetched.
// app.js
import React from "react";
import { mutate } from "swr";
import Loading from "./loading";
import fetcher from "./fetcher";
const sleep = ms => new Promise(r => setTimeout(r, ms));
const LazyResource = React.lazy(() =>
sleep(1000).then(() => import("./resource"))
);
export default function App() {
const [id, setID] = React.useState(null);
async function handleChange(event) {
const newID = event.target.value;
mutate(`/api/resource/${newID}`, fetcher(`/api/resource/${newID}`), false);
setID(newID);
}
return (
<>
<label htmlFor="id">Resource ID:</label>{" "}
<input id="id" type="text" onChange={handleChange} value={id} />
{!id ? (
<p>Enter ID</p>
) : (
<React.Suspense fallback={<Loading />}>
<LazyResource id={id} />
</React.Suspense>
)}
</>
);
}
If we check our App
component, we removed the usage of useSWR
because we moved it back to Resource
, but we imported a function called mutate
from SWR.
This little function lets us update the data cached by SWR in a certain key. To do so we need to call it passing the cache key, the URL in our case, the data and if we want SWR to revalidate it against our API, the last option which is enabled by default is useful to support Optimistic UI updates, in our case since we are going to just get the data from the API we don't need to revalidate it, so we pass false
.
But the most important part here is the second argument, I wrote above we need to pass the data there, but we are instead passing a Promise object, without waiting for it to resolve. This works because mutate
realize we send a Promise and it will internally wait for it to resolve. Thanks to that we could trigger the fetch and update the input value immediately.
Let's see now how it works in CodeSandbox with these changes.
As you can see now the time to render the URL on the screen is only one second! That's amazing because that means we are getting the code and the data required to render the component at the same time. Our Resource
component is then reusing the data previously fetched and rendering right away, without needing to wait for another second to get the data.
Try playing with the fake delays in the lazy loading and the data fetching, you will see how we are only waiting for the longest delay and not for both of them combined.
Top comments (0)