Brian Mwangi

Back to articles

Persist form inputs across page reloads

-- 2 min read

An image of cookies lined with chocolate

It's annoying when a page refreshes when you were filling in a form; you lose all your data and now you have to start all over 😐.

Persisting the values in form inputs as a user is filling in a form creates a great user experience. In this article, we will learn how to persist values in form inputs using cookies. If you're unfamiliar with cookies, learn more about them here.

Project setup

In this project I will be using Remix framework, but you're free to use whichever framework you're comfortable with; the fundamentals are the same. Let's start by initializing a Remix application:

npx create-remix@latest

Remix has great apis for handling sessions and cookies. We will use the built-in utility createCookieSessionStorage() to create a session based cookie. Let's create a file session.js and initialize the session storage object.

// app/.server/session.js

import { createCookieSessionStorage } from "@remix-run/node"; // or cloudflare/deno

export let { getSession, commitSession, destroySession } =
  createCookieSessionStorage({
    // a Cookie from `createCookie` or the same CookieOptions to create one
    cookie: {
      name: "checkout__session",
      secrets: ["s3cr345"],
      sameSite: "lax",
      path: '/',
      maxAge: 60 * 60 * 24,
      httpOnly: true,
      secure: true,
    },
  });

The function createCookieSessionStorage() returns a session storage object which has three methods ( getSession(), commitSession(), and destroySession() ) that will help us interact with cookies and the data in the cookies.

Add user input to the cookie

Now that we have a session storage object, let's use it to add the user data to the cookie. We want to save changes as the user is using the form, therefore we have to submit the input values to the server and save them in a session cookie whenever an input receives focus then loses it.

We do this in Remix by submitting the form in an input's onBlur event handler using the useSubmit() hook.

// app/routes/checkout.jsx

import { json, useLoaderData, useSubmit } from "@remix-run/react";
import { getSession, commitSession } from "~/.server/session";

export async function action({ request }) {
  let session = await getSession(request.headers.get('Cookie'));
  
  let formData = await request.formData();
  let name = formData.get('name');
  let email = formData.get('email');

  let userData = { name, email };

  // Put the user details in the session
  session.set('userData', userData);
  
  return json({ ok: true }, {
    headers: {
      "Set-Cookie": await commitSession(session)
    }
  });
  
}
export default function Checkout() {
  let submit = useSubmit();
  
  return (
    <main>
      <hi>Checkout</h1>
      <form method="post">
        <fieldset>
          <label>
            Name
            <input 
              type="text" 
              name="name"
              // Submit the form when the user leaves the input
              onBlur={(event) => { submit(event.currentTarget.form )}}
            />
          </label>
          <label>
            Email
            <input 
                type="email" 
                name="email"
              // Submit the form when the user leaves the input
              onBlur={(event) => { submit(event.currentTarget.form )}}
                
            />
          </label>
          <button type="submit">Check out</button>
        </fieldset>
      </form>
    </main>
  );
}

Use cookie values as default inputs

Finally we use the input values from the cookie as the default values for the input fields. We get the values from the loader function and pass them to the component to be used.

// app/routes/checkout.jsx

import { useLoaderData } from "@remix-run/react";
import { getSession } from "~/.server/session";

export async function loader({ request }) {
  let session = await getSession(request.headers.get('Cookie'));

  // Retrieve the user data that was set in the session
  let userData = session.get('userData') ?? null;

  return userData;
}

export default function Checkout() {
  // Get the user data from the loader
  let userData = useLoaderData();
  
  return (
    <main>
      <h1>Checkout</h1>
      <form method="post">
        <fieldset>
          <label>
            Name
            <input 
              type="text"
              name="name"
              // Use the value from the cookie as the default value
              defaultValue={userData?.name} 
            />
          </label>
          <label>
            Email
            <input 
              type="email"
              name="email"
              // Use the value from the cookie as the default value
              defaultValue={userData?.email} 
            />
          </label>
          <button type="submit">Check out</button>
        </fieldset>
      </form>
    </main>
  );
}

This way we prevent unnecessary loss of user input data, leading to a more satisfying user experience.

Final outcome

This is how the experience looks like after implementing the tricks that we've learnt today.

WhatsApp icon