Use SWR with React Suspense
This article will explore how you should use the awesome useSWR hook for remote data fetching.
Why the SWR hooks for remote data fetching?
Because SWR (stale-while-revalidate) is a fully dedicated React Hooks library for remote data fetching. So basically, this is what SWR does:
- First returns the data from cache (stale)
- Then sends the fetch request (revalidate).
- Finally comes with the up-to-date data again.
Why Suspense?
Because Suspense will definitely help you to maintain a consistent UI in the face of asynchronous dependencies. Something I prefer to call on-demand loaded React components.
All you need is to:
- Set the suspense: true as one of the useSWR hook options.
- Wrap your on demand route component inside of its own suspense component.
import React, { Suspense } from "react";
import ReactDOM from "react-dom";
import useSWR from "swr";
const App = () => {
return (
<Suspense fallback={<FallbackProgress />}>
<Page />
</Suspense>
);
};
const FallbackProgress = () => <div>Loading...</div>;
const Page = () => {
const { data } = useSWR(
"https://jsonplaceholder.typicode.com/Todos/1",
(req) => fetch(req).then((res) => res.json()),
{ suspense: true }
);
return <div>{data.title}</div>;
};
const rootElement = document.getElementById("root");
ReactDOM.createRoot(rootElement).render(<App />);
The whole thing put together
- Github – components/States/States.js
import React, { Suspense, lazy } from "react";
import { Progress } from "components";
const StatesLazy = lazy(() => import("./States.lazy"));
const States = () => (
// Progress Component contains `data-testid="id-request-progress"`
<Suspense fallback={<Progress />}>
<div data-testid="id-states-lazy">
<StatesLazy />
</div>
</Suspense>
);
export default States;
- Github – components/States/States.lazy.js
import React, { useEffect } from "react";
import Container from "@material-ui/core/Container";
import Divider from "@material-ui/core/Divider";
import { Link } from "react-router-dom";
import Typography from "@material-ui/core/Typography";
import { Redirect, useHistory } from "react-router-dom";
import _get from "lodash.get";
import useSWR from "swr";
import {
useSearchValueDispatch,
useSearchValueState,
} from "context/SearchValue";
import { endpoints } from "config";
import { searchUtil } from "utils";
import { csv2objFetcherService } from "services";
import { ContentCard, ContentMessage } from "components";
import useStyles from "./States.lazy.style";
const StatesLazy = () => {
const classes = useStyles();
const history = useHistory();
const { searchValue } = useSearchValueState();
const dispatch = useSearchValueDispatch();
const requestURLConst = "for=state:*&DATE_CODE=1";
const { data } = useSWR(
`${endpoints.mainURL}${requestURLConst}`,
csv2objFetcherService,
{ suspense: true }
);
const response = {
status: _get(data, "status", 200),
data: _get(data, "data", []),
error: _get(data, "error", ""),
};
useEffect(() => {
dispatch({ type: "setSearchValueReducer", payload: "" });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (response.status !== 200 && response.error !== "") {
return (
<div data-testid="id-states-response-error">
<Redirect
to={{
pathname: history.location.pathname,
state: { status: response.status, error: response.error },
}}
/>
</div>
);
}
const searchResults = searchUtil(response.data, searchValue);
if (searchResults && searchResults.length === 0) {
return (
<div data-testid="id-states-no-search-results">
<ContentMessage
type="message"
title="No Results Found!"
description="Let's ask again."
/>
</div>
);
}
return (
<Container
data-testid="id-states-container"
className={classes.root}
maxWidth="md"
>
<Typography variant="h1" className={classes.title}>
States
</Typography>
<Divider className={classes.divider} />
{searchResults.map(
(state) =>
state.NAME &&
state.state && (
<Link
// Key
key={Number(state.state)}
// Rest of the Props
className={classes.link}
to={`${Number(state.state)}/counties`}
>
<ContentCard
// Key
key={Number(state.state)}
// Rest of the Props
density={state.DENSITY}
population={state.POP}
title={state.NAME}
/>
</Link>
)
)}
</Container>
);
};
export default StatesLazy;
Wrapping up
As long as there is a child component using promises or acting asynchronous inside the Suspense component, it will be replaced by the component defined in the fallback prop.