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

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;
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.