Create Client-Only Routes

While Gatsby is extremely powerful at prerendering content, it's also great for client-only apps. With the addition of dynamic routing in Gatsby 3, it's delightfully straightforward to add a client-only route to your Gatsby site.

For this project, let's create a book search. This relies on user input, so there's no way for us to know what content will be on the page ahead of time — it needs to be dynamically generated.

Create styles for the client-only route

We'll need a few styles handy for the search form, so create a new file at site/src/styles/search.module.css and add the following:

.form {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
}

.input {
  font-size: 1rem;
  padding: 0.25rem 1rem;
  width: 100%;
}

.button {
  font-size: 1rem;
  padding: 0.25rem 1rem;
}

Create a client-only dynamic page

To create client-only pages, we use square brackets around a parameter name in the page filename. For example, if we wanted to get someone's username out of the URL, we could create a filename like /users/[username].js.

For this example, we're going to use a catch-all dynamic route inside a folder, which will cause every URL that starts with our folder name to be routed to this component.

Create a new file at site/src/pages/search/[...].js with the following content:

import * as React from "react";
import { navigate } from "gatsby";

import { form, input, button } from "../../styles/search.module.css";

export default function BookClientOnly({ params }) {
  const query = decodeURIComponent(params["*"]);
  const [currentQuery, setCurrentQuery] = React.useState(query);
  const [result, setResult] = React.useState(null);
  const [status, setStatus] = React.useState("IDLE");

  function handleSearch(event) {
    event.preventDefault();

    const form = new FormData(event.target);
    const query = form.get("search");

    setCurrentQuery(query);
    navigate(`/search/${encodeURIComponent(query)}`);
  }

  function handleSearchReset(event) {
    setCurrentQuery("");
    navigate("/search/");
  }

  async function bookSearch(query) {
    setStatus("LOADING");
    const res = await fetch(`https://openlibrary.org/search.json?q=${query}`);

    if (!res.ok) {
      throw new Error(`Search failed: ${res.status}`);
    }

    const result = await res.json();

    setResult(result);
    setStatus("IDLE");
  }

  React.useEffect(() => {
    if (currentQuery === "") {
      setResult(null);
      return;
    }

    bookSearch(currentQuery);
  }, [currentQuery]);

  return (
    <>
      <h1>Search for a Book</h1>
      <form className={form} onSubmit={handleSearch}>
        <input className={input} type="search" name="search" />
        <button className={button}>search</button>
        <button className={button} type="reset" onClick={handleSearchReset}>
          reset
        </button>
      </form>

      {status === "LOADING" && <p>Loading results...</p>}

      {status === "IDLE" && currentQuery !== "" ? (
        <>
          <h2>Search results for "{currentQuery}":</h2>
          <ul>
            {result &&
              result.docs.map((doc) => (
                <li key={doc.key}>
                  <strong>{doc.title}</strong>{" "}
                  {doc.author_name && `by ${doc.author_name?.[0]}`}
                </li>
              ))}
          </ul>
        </>
      ) : null}
    </>
  );
}

Create a client-only fallback route to redirect to the dashboard

If someone hits either the root (/account) or a URL that doesn't exist inside the account path, we want to redirect them to the dashboard.

To do that, let's create a fallback catch-all route at site/src/pages/account/[...].js:

import { navigate } from "gatsby";

export default function RedirectToAccountDashboard() {
  navigate("/account/dashboard", { replace: true });

  return null;
}