ReactHustle

Build Multi-Page Forms with Formik and NextJS

Jasser Mark Arioste

Jasser Mark Arioste

Build Multi-Page Forms with Formik and NextJS

Hello, hustlers! In this tutorial, you will learn how to build a multi-page form with Formik. You will learn how to retain the form state across multiple tabs using the Formik provider and NextJS shallow routing.

The Problem #

In one of my projects, I was tasked with building a CMS backend for the PostDetail page. A post was split into multiple tabs and routes, and there is one "Update Post" button that is visible for all the tabs. The "Update Post" button is disabled if there are no changes across multiple tabs.

In addition, each tab will have one separate route. For example, `/cms/post/{slug}/seo` for the SEO metadata, and `/cms/post/{slug}/content` for the post content. If there are any modifications to either one of these, the "Update Post" button should be enabled.

Error:  The Parser function of type "raw" is not defined. Define your custom parser functions as: https://github.com/pavittarx/editorjs-html#extend-for-custom-blocks 

Now that we know what we want, let's start this mini-project. We'll follow these steps:

  1. Project Setup
  2. Creating the Tabs component for navigation: /cms/posts/{slug}, cms/posts/{slug}/content, /cms/posts/{slug}/seo
  3. Creating form components for each route.
  4. Creating the "Update Post" button.
  5. Adding Initial values
  6. Modifying the "Update Post" button behavior

Step 1 - Project Setup - Formik + NextJS #

I created a GitHub repo If you want to follow along with this tutorial. I use tailwind and daisyUI for styling but you can modify it if you'd like. You can immediately set it up in your local environment by running the command:

npx create-next-app -e https://github.com/jmarioste/formik-multi-page-tutorial/tree/starting-point formik-tutorial

1

Next, you can run it by doing:

cd formik-tutorial && yarn dev
1

This will start a server in your local environment, then you can go to http://localhost:3000

Formik multi-page tutorial

Step 1.1 - Adding Formik and classnames package

Before we move on to step 2, let's add formik and classnames package. Formik is the main dependency for this project, while classnames package just makes our lives easier when manipulating classes.

Run the command:

yarn add formik classnames
1

Step 2 - Creating the Tabs Component for Navigation #

In this step, you will create the Tabs, as well as the page routes.

Next, we'll create the PostDetailTabs component, which will handle navigation between tabs. Create a file components/post/PostDetailTabs.tsx

import React from 'react'
import classNames from "classnames";
import Link from "next/link";
import { useRouter } from 'next/router';
const PostDetailTabs = () => {
  const router = useRouter();
  const params = Array.isArray(router.query.slug)
    ? (router.query.slug as string[])
    : [];
  const [slug, currentTab = ""] = params;
  const tabInfo = [
    { url: `/cms/post/${slug}`, text: "PostInfo", activeMatcher: "" },
    { url: `/cms/post/${slug}/content`, text: "Content", activeMatcher: "content" },
    { url: `/cms/post/${slug}/seo`, text: "SEO Metadata", activeMatcher: "seo" }
  ]
  return (
    <div>
      <div className="tabs">
        {
          tabInfo.map((tab, index) => {
            return <Link href={tab.url} key={index} shallow={true}>
              <a className={classNames({
                "tab tab-bordered": true,
                "tab-active": tab.activeMatcher === currentTab,
              })}>{tab.text}</a>
            </Link>
          })
        }
      </div>
    </div>
  )
}
export default PostDetailTabs
123456789101112131415161718192021222324252627282930313233

Explanation:

Lines 6-8: we parse the params from [...slug], we're expecting this to include the slug itself and the currentTab.

Lines 10-14: We define data for our tabs.

Lines 19-26: We render the data. We use NextJS <Link/> component, and use shallow={true} along with daisyUI classes for styling.

Next, create the components/post/PostDetail.tsx file. Later, we will add initialValues and a proper onSubmit handler that simulates an API call.

// components/post/PostDetail.tsx
import { Formik } from "formik";
import React from "react";
import PostDetailTabs from "./PostDetailTabs";
const PostDetail = () => {
  return (
    <Formik initialValues={{}} onSubmit={(values) => { console.log("onSubmit", values) }}>
      <div className="container">
        <h1 className="text-2xl font-bold my-2">Update Post</h1>
        <PostDetailTabs />
      </div>
    </Formik >
  );
};
export default PostDetail;

123456789101112131415

Line 7: For now, we initialize formik with empty values just to satisfy the props. Remember we're still building the UI.

First, create a file: `/pages/cms/post/[...slug].tsx`. This is our route handler to update each post.

// pages/cms/post/[...slug].tsx
import PostDetail from 'components/post/PostDetail'
import React from 'react'
export default PostDetail
1234

Note that we only exported the PostDetail component here. You might wonder why. I find it much easier to place all the components inside the /components folder, including the ones for routing. This is especially useful in a large app.

After this is done, you can navigate to http://localhost:3000/cms/post/hello-world to see the current output:

Formik Implementing the Tabs&nbsp; components - output After Step 1

Step 3 - Creating form components for each route. #

Step 3.1 - Defining the <PostInfoTab/> component

Let's quickly add form inputs for each route. First, create the <PostInfoTab/> component, which will contain headline and slug fields. Create the file `/components/post/PostInfoTab.tsx`.

// components/post/PostInfoTab.tsx
import { Field } from 'formik'
import React from 'react'
const PostInfoTab = () => {
  return (
    <div>
      <div className="form-control">
        <label className='label text-accent'>
          <span>Headline</span>
        </label>
        <Field name="headline" type="text" className="input input-bordered" placeholder="headline" />
        <label className='label text-accent'>
          <span>Slug</span>
        </label>
        <Field name="slug" type="text" className="input input-bordered" placeholder="slug" />
      </div>
    </div>
  )
}
export default PostInfoTab
1234567891011121314151617181920

Step 3.2 - Defining the <PostContentTab/> component

Create the file `/components/post/PostContentTab.tsx`. This will contain the post content itself.

// components/post/PostInfoTab.tsx
import { Field } from 'formik'
import React from 'react'
const PostContentTab = () => {
  return (
    <div>
      <div className="form-control">
        <label className='label text-accent'>
          <span>Content</span>
        </label>
        <Field as="textarea" name="content" className="input input-bordered" placeholder="Write a great article..." />
      </div>
    </div>
  )
}
export default PostContentTab
12345678910111213141516

Step 3.3 - Defining the <PostSeoMetadataTab/> component

Create the file `/components/post/PostSeoMetadataTab.tsx`. This will contain SEO metadata fields.

import { Field } from 'formik'
import React from 'react'
const PostSeoMetadataTab = () => {
  return (
    <div>
      <div className="form-control">
        <label className='label text-accent'>
          <span>Page Title</span>
        </label>
        <Field name="title" type="text" className="input input-bordered" placeholder="title" />
        <label className='label text-accent'>
          <span>Meta Description</span>
        </label>
        <Field name="description" type="text" className="input input-bordered" placeholder="description" />
      </div>
    </div>
  )
}
export default PostSeoMetadataTab
12345678910111213141516171819

Step 3.4 - Updating <PostDetailTabs/> Component

Let's update <PostDetailTabs/> component to render the correct Tab component depending on the route:

import React, { useMemo } from 'react'
...
import PostContentTab from './PostContentTab';
import PostInfoTab from './PostInfoTab';
import PostSeoMetadataTab from './PostSeoMetadataTab';
const PostDetailTabs = () => {
  ...
  const TabComponent = useMemo(() => {
    switch (currentTab) {
      case "content": return <PostContentTab />;
      case "seo": return <PostSeoMetadataTab />;
      default:
        return <PostInfoTab />
    }
  }, [currentTab])
  return (
    <div>
      <div className="tabs">
        {
          ...
        }
      </div>
      {TabComponent}
    </div>
  )
}
export default PostDetailTabs
123456789101112131415161718192021222324252627

Now we have field inputs depending on the current tab / route:

Adding each fields component

Step 4 - Creating UpdatePostButton Component #

For step 4, we'll create the <UpdatePostButton/> component. For now, let's implement the UI, well add functionality later. Create the file /components/post/UpdatePostButton.tsx.

import React from 'react'

const UpdatePostButton = () => {
  return (
    <div className='fixed bottom-4 right-4'>
      <button type="submit" className='btn btn-primary'>Update Post</button>
    </div>
  )
}

export default UpdatePostButton
1234567891011

Next, let's update <PostDetailTabs/> component to render the <UpdatePostButton/> component:

// components/post/PostDetailTabs.tsx
..
import UpdatePostButton from './UpdatePostButton';
const PostDetailTabs = () => {
  ...
  return (
    <div>
      <div className="tabs">
        ...
      </div>
      {TabComponent}
      <UpdatePostButton />
    </div>
  )
}

export default PostDetailTabs
1234567891011121314151617

After this step, we've completed all the necessary UI. Looking good!

Output after adding Update post button

Step 5 - Formik initialValues and onSubmit #

Next, we'll implement the initialValues and onSubmit handler. Modify the <PostDetail/> component as below:

// components/post/PostDetail.tsx
import { Form, Formik } from "formik";
import React from "react";
import PostDetailTabs from "./PostDetailTabs";
const PostDetail = () => {
  const initialValues = {
    slug: "hello-world",
    headline: "Hello, world",
    content:
      "In this tutorial, you will learn about hello, world in javascript.",
    seo: {
      title: "Hello, world | React Hustle",
      description:
        "In this tutorial, you will learn about hello, world in javascript.",
    },
  };
  return (
    <Formik
      initialValues={initialValues}
      onSubmit={(values, helpers) => {
        helpers.setSubmitting(true);
        console.log("onSubmit", values);
        //you should call backend api here. 
        //setTimoue to simulate api call
        setTimeout(() => {
          helpers.setSubmitting(false);
          helpers.resetForm({ values })
        }, 2000);
      }}
    >
      <Form className="container">
        <h1 className="text-2xl font-bold my-2">Update Post</h1>
        <PostDetailTabs />
      </Form>
    </Formik>
  );
};
export default PostDetail;
1234567891011121314151617181920212223242526272829303132333435363738

Lines #6-16: We hardcode the initialValues. in a real app, these values are fetched from your backend API.

Line #19: We pass the initialValues to the Formik provider.

Line #21-29: First we set the form state to submitting. And to simulate a backend API call, we use setTimeout and reset the form with the updated values. This is useful since we will disable the <UpdatePostButton/> component if the form is dirty. By resetting the form with the updated values, we set the form not dirty

Step 6 - Modifying <UpdatePostButton/> #

// components/post/UpdatePostButton.tsx
import classNames from 'classnames';
import { useFormikContext } from 'formik';
import React from 'react'
const SaveButton = () => {
  const { dirty, isSubmitting } = useFormikContext();
  return (
    <div className='fixed bottom-4 right-4'>
      <button type="submit" className={
        classNames({
          'btn btn-primary': true,
          'loading': isSubmitting,
        })
      } disabled={!dirty}>
        Update Post
      </button>
    </div>
  )
}

export default SaveButton
123456789101112131415161718192021

Explanation:

Line 6: We use useFormikContext hook from formik to get the two pieces of state that we want. dirty and isSubmitting

Line 12: We add a loading class if the form is being submitted.

Line 14: We disable the button if the form is not dirty. dirty means that at least one of the fields has been modified.

That's it.

This works because all our tabs are inside <Formik> provider, all the state is preserved since we're using shallow routing between tabs. 

Final Output #

Formik Multi-Page Form Final output

Conclusion #

With the combination of NextJS and Formik, we learned how to preserve "Form state" across different routes. We can navigate through the tabs safely with all our data intact.

Also, the user is able to bookmark a tab since it is saved in the browser address path. There might be some cases where this is useful for your application. 

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter.

Resources #

Related Posts #

Credits: Image by Adam Derewecki from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle