DEV Community

RafaƂ GoƂawski
RafaƂ GoƂawski

Posted on

Handling Layout Form Submission Inside a React Router Cookie Banner đŸȘ

Recently, I was working on a React Router v7 app where I needed to implement a cookie banner. The app has many routes, and the requirement was to display the cookie banner on every single one of them, without exception. As all the routes share the same layout, it felt natural to add the cookie banner there. In this article, I’ll walk you through how to achieve this.

Step 1: Setting Up the Layout Component

First, let’s create a layout component and define how it will be used in our routes. Here’s a simple example:

// app/layouts/layout.tsx
import { Outlet } from "react-router";

export function Layout() {
  return (
    <>
      <header>...</header>
      <main>
        <Outlet />
      </main>
      <footer>...</footer>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Then, we use the layout in our routing configuration:

// app/routes.ts
import { type RouteConfig, index, layout } from "@react-router/dev/routes";

export default [
  layout("layouts/layout.tsx", [index("routes/home.tsx")]),
] satisfies RouteConfig;
Enter fullscreen mode Exit fullscreen mode

Now that we have our layout set up, we can move on to creating the cookie banner.

Step 2: Creating the Cookie Banner Component

Let’s start by building a simple cookie banner:

// app/components/cookie-banner.tsx
import { Form } from "react-router";

export function CookieBanner() {
  return (
    <div>
      <p>We're using cookies</p>
      <p>...</p>
      <Form method="POST">
        <button type="submit" name="consent" value="all">
          Accept all
        </button>
        <button type="submit" name="consent" value="necessary">
          Only necessary
        </button>
      </Form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, we use the <Form> component from React Router for issuing POST requests when users interact with the cookie banner. We’ll revisit the Form in a moment.

Step 3: Adding the Cookie Banner to the Layout

Now that the cookie banner component is ready, we can include it in the layout to make it appear across all routes:

// app/layouts/layout.tsx
import { Outlet } from "react-router";
import { CookieBanner } from "~/components/cookie-banner";

export default function Layout() {
  return (
    <>
      <header>...</header>
      <main>
        <Outlet />
      </main>
      <footer>...</footer>
      <CookieBanner />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

At this point, the cookie banner will render on every page of the app. Try clicking the buttons, however, and you’ll encounter a “Method Not Allowed” error. Let’s fix that.

Step 4: Handling Form Actions

The issue stems from missing an action attribute in the <Form>. If you don’t specify an action, the form’s request is routed to the current page, and React Router doesn’t know how to handle it.

A simple solution is to point the action to the index route (or any other route that handles the logic). Update the form as follows:

// app/components/cookie-banner.tsx
<Form method="POST" action="/?index">
Enter fullscreen mode Exit fullscreen mode

Now, when the form is submitted, the request will be sent to the index route’s action. Let’s define that action to handle the form submission:

// app/routes/home.tsx
export function action() {
  // Handle user cookie preferences here
  return {};
}
Enter fullscreen mode Exit fullscreen mode

For simplicity, this example doesn’t include the logic to store user preferences. If you’d be interested in learning more about that, let me know in the comments - this could be a topic for another article!

Step 5: Improving User Experience: Redirecting Back

Currently, when users submit the form from a route that isn’t the index route, they’ll be redirected back to the index route after submission. This isn’t ideal from a user experience perspective, so let’s fix it.

The goal is to redirect users back to the page where they submitted the form. To achieve this, we’ll capture the current page’s pathname using the useLocation hook and send it as part of the form data. Update the cookie banner component like so:

// app/components/cookie-banner.tsx
import { Form, useLocation } from "react-router";

export function CookieBanner() {
  const { pathname } = useLocation();
  return (
    <div>
      <p>We're using cookies</p>
      <p>...</p>
      <Form method="POST" action="/?index">
        <input type="hidden" name="redirect-to" value={pathname} />
        <button type="submit" name="consent" value="all">
          Accept all
        </button>
        <button type="submit" name="consent" value="necessary">
          Only necessary
        </button>
      </Form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Using the pathname value, we include a hidden input field named redirect-to. This ensures the form submits the current page’s URL.

Next, let’s modify the index route’s action to handle this value and redirect the user appropriately:

// app/routes/home.tsx
import { redirect } from "react-router";
import type { Route } from "./+types/home";

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const redirectTo = String(formData.get("redirect-to") || "/");
  // Handle user preferences here
  return redirect(redirectTo);
}
Enter fullscreen mode Exit fullscreen mode

With this setup, users will be redirected back to the page they were on when they submitted their cookie preferences.

Step 6: Conditional Rendering of the Cookie Banner

Another important improvement is ensuring the cookie banner is dismissed once the user interacts with it. We don’t want the banner to reappear every time the page reloads. To achieve this, we can use a loader in the layout to fetch user preferences and render the banner conditionally.

Here’s an example implementation:

// app/layouts/layout.tsx
import { Outlet } from "react-router";
import { CookieBanner } from "~/components/cookie-banner";
import type { Route } from "./+types/layout";

export function loader() {
  return { consent: "unset" }; // Fetch user preferences and return them here
}

export default function Layout({ loaderData }: Route.ComponentProps) {
  return (
    <>
      <header>...</header>
      <main>
        <Outlet />
      </main>
      <footer>...</footer>
      {loaderData.consent === "unset" && <CookieBanner />}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the loader fetches the user’s cookie consent preferences (this logic will depend on your implementation). If the user has already provided consent, the cookie banner will not render. If no consent has been provided (consent: "unset"), the banner is displayed.

This ensures the banner’s behavior is consistent across different routes and reloads.

Wrapping Up

In this article, we implemented a cookie banner in a React Router v7 app that seamlessly works across all routes. We tackled:

  • Adding a layout component and integrating a cookie banner into it.
  • Handling form submissions and redirecting users back to their previous page.
  • Conditionally rendering the cookie banner based on user preferences.

By using React Router’s loader and action mechanisms, we achieved a clean and user-friendly solution. If you’d like a follow-up article exploring how to persist cookie preferences (e.g., using local storage, an API, or cookies), let me know in the comments - I’d be happy to dive deeper into the topic.

Thanks for reading!

Top comments (0)