Table of contents

  • Create a project
  • Create form steps
  • Implement client-only routes
  • Demo

How to create a multistep form with gatsby

January 2015

4 min

I am currently rewriting a 10+ year old website written in ASP.NET to React/GatsbyJS. The website had multiple forms with more than 50 input fields, some which could be omitted but a large portion of the inputfields is vital for the price caluclation.
Recently I discovered react-hook-form which is very intutive to work with, minimizes the number of re-renders and in general just make it much more pleasant to work with forms, I will probably write a small tutorial in the future to show how I use it.

However, the decision to split the huge form into a multistep form was quickly made but I had no idea how to actually implement this with gatsbyjs. After some investigation also called google, I decided to use dynamic routes to create the steps in the form and at last recoil to manage the state of the form.

Create a project

We are going to use the default gatsby starter for this project. I would advice any one who starts a new gatsby project to start to from scratch to have the most control an knowlgde about the project.

gatsby new multiform
yarn add recoil @reach/router react-hook-form

We now have project with the following structure:

├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── gatsby-ssr.js
├── package-lock.json
├── package.json
└── src
    ├── components
    │   ├── header.js
    │   ├── layout.css
    │   ├── layout.js
    │   └── seo.js
    ├── images
    │   ├── gatsby-astronaut.png
    │   └── gatsby-icon.png
    └── pages
        ├── 404.js
        ├── index.js
        ├── page-2.js
        └── using-typescript.tsx

Create form steps

We are going to create a new page named form

touch ./src/pages/form.jsx

We are going to create three steps in our form, Step1 which contains first and last name, Step2 which has an email field and the last step is going to be a FormSummary, I won't go into the details about how recoil works, but if you wan't know more about it you can read this post which explains how it can be implemented in Gatsby.

import React from "react";
import { navigate } from "gatsby";
import { useForm } from "react-hook-form";
import { useRecoilState } from "recoil";
import { formState } from "./state";

export const Step1 = () => {
  const { register, handleSubmit } = useForm();
  const [formData, setFormData] = useRecoilState(formState);

  const onSubmit = (data) => {
    setFormData({ });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>   
      <input type="text" {...register("first_name")} placeholder="first name" />
      <input type="text" {...register("last_name")} placeholder="last name" />
      <input type="submit" />

export const Step2 = () => {
  const { register, handleSubmit } = useForm();
  const [formData, setFormData] = useRecoilState(formState);

  const onSubmit = (data) => {
    setFormData({ ...formData, });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input type="email" {...register("email")} placeholder="email" />
      <input type="submit" />

export const FormSummary = () => {
  const [formData, setFormData] = useRecoilState(formState);
  return (
      <pre>{JSON.stringify(formData, null, 2)}</pre>

Implement client-only routes

Theese are the three steps we are going to use, the next thing is to create a page for our form, I am going to create a form.js page in the /src/pages like this

import React from "react";
import Layout from "../components/layout";
import { Router } from "@reach/router";
import { FormSummary, Step1, Step2 } from "../components";

const FormPage = () => {
  return (
        <Step1 path="/form/step1" />
        <Step2 path="/form/step2" />
        <FormSummary path="/form/summary" />

export default FormPage;

Starting the project and navigating to http://localhost:8000/form will give you a blank screen. We still need to create the client-only routes through gatsby-node.js in order to make gatsby recognize the custom routes which we defined in the form page. If you wan't to know how this work I suggest you read the docs.

exports.onCreatePage = async ({ page, actions }) => {
  const { createPage } = actions;

  if (page.path.match(/^\/form/)) {
    page.matchPath = `/form/*`;

And that's is, if you would like to have a bac button you can simoly make one, just be sure to