The evolution of React JS: React Router V7 and SSR done right

This is gonna be a quick one, the idea is to give a summary of the conference of React 2024 going on right now, one in particular that caught my attention was the one done showcasing how to enhance your forms using React Server Action.

This is from React conference in 2024:

https://www.youtube.com/watch?v=ZcwA0xt8FlQ

Here is the repo: https://github.com/ryanflorence/demo-notes-app/tree/rr7

Defining routes in RR7

For starters, let’s check how we define routes now:

import { defineConfig } from "vite";
import { vitePlugin as react } from "@react-router/dev";
import inspect from "vite-plugin-inspect";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react({
      appDirectory: "src",
      ssr: false, // THIS TURNS ON OR OFF THE SSR FEATURE
      future: {
        unstable_singleFetch: true,
      },
      // below is SSG
      //prerender() {
      //  return ["/store"]
      //},
      routes(defineRoutes) {
        return defineRoutes((route) => {
          route("", "containers/Home.tsx", { index: true });
          route("/login", "containers/Login.tsx");
          route("/signup", "containers/Signup.tsx");
          route("/settings", "containers/Settings.tsx");
          route("/notes/new", "containers/NewNote.tsx");
          route("/notes/:id", "containers/Notes.tsx");
          route("*", "Routes.tsx");
        });
      },
    }),
    inspect(),
  ],
});

What we’re doing now is switch the features of Remix and Vite Plugin towards React Router and we get the above!

But, how do we define our routes then? Easy!

import { Route, Routes } from "react-router";
import NotFound from "./containers/NotFound.tsx";

export default function Links() {
  return (
    <Routes>
      <Route path="*" element={<NotFound />} />;
    </Routes>
  );
}

By defining it inside Vite config, it will get read automatically because its code splitting the route we define in the vite config.

Now for the big update, lets check clientLoader.

Client Loader

This new function is meant to handle asynchronous operations and it will always run BEFORE the component gets rendered, which is big because we don’t have to do it inside the component itself and do the usual check operations for errors and have the code get dirty, now we can do it separately!

Let’s check first how it looks like:

export async function clientLoader({ request, params }: CLFA) {
  await requireAuth(request);
  const note = await API.get("notes", `/notes/${params.id}`, {});
  if (note.attachment) {
    note.attachmentURL = await Storage.vault.get(note.attachment);
  }
  return note;
}

This will always run before Notes. We can read this by using useLoaderData from React Router and we read its typing. Below we show how the component looks like:

export default function Notes() {
  const note = useLoaderData<typeof clientLoader>();
  const fetcher = useFetcher();

  function formatFilename(str: string) {
    return str.replace(/^\w+-/, "");
  }

  const confirm = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
    if (!window.confirm("Are you sure you want to delete this note?")) {
      e.preventDefault();
    }
  };

  return (
    <div className="Notes">
      {note && (
        <fetcher.Form method="post" encType="multipart/form-data">
          <Stack gap={3}>
            <Form.Group controlId="content">
              <Form.Control
                defaultValue={note.content}
                size="lg"
                as="textarea"
                name="content"
                required
              />
            </Form.Group>
            <Form.Group className="mt-2" controlId="file">
              <Form.Label>Attachment</Form.Label>
              {note.attachment && (
                <p>
                  <a
                    target="_blank"
                    rel="noopener noreferrer"
                    href={note.attachmentURL}
                  >
                    {formatFilename(note.attachment)}
                  </a>
                </p>
              )}
              <input
                type="hidden"
                name="attachment"
                defaultValue={note.attachment}
              />
              <Form.Control type="file" name="newAttachment" />
            </Form.Group>
            <Stack gap={1}>
              <LoaderButton
                size="lg"
                type="submit"
                name="intent"
                value="save"
                isLoading={fetcher.formData?.get("intent") === "save"}
              >
                Save
              </LoaderButton>
              <LoaderButton
                size="lg"
                variant="danger"
                name="intent"
                value="delete"
                isLoading={fetcher.formData?.get("intent") === "delete"}
                onClick={confirm}
              >
                Delete
              </LoaderButton>
            </Stack>
          </Stack>
        </fetcher.Form>
      )}
    </div>
  );
}

React Server Components

In the Vite config we can turn ON the SSR option and it enables the rendering in the server, for example we can do:

export async function loader() {
  let products = await getTopProducts()
  return (
    <div>
      {products.map((product: Product) => (
        <div key={product.handle}>
          <h2>{product.title}</h2>
          <Carousel media={product.media.edges} />
        </div>
      ))}
    </div>
  )
}

export default function Store() {
  let products = useLoaderData() as any

  return (
    <div>
      <h1>Store</h1>
      {products}
    </div>
  )
}

Do note that we switch from clientLoader to loader which indicates that we’re rendering it on the server and not the client.

This is absolutely FANTASTIC! We can finally use SSR without resorting only to NextJS and all its functionalities and performance issues that bring more pain that gain, with due experience you usually get around it, and its needed to be learned in 2024, but this new option will be really great in the future. But this also means we need, as a community, to define the correct way to work with these new features as this is not a framework like NextJS anymore and its a library, we need to take into account scalability when we define the way to structure our projects.

React Server Components and SSR are coming to Vanilla React in the form of React Router V7!

See you on the next post.

Sincerely,

Eng. Adrian Beria.