React Hustle

Build Multi-Page Forms with Formik and NextJS

JA

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.

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

Next, you can run it by doing:

cd formik-tutorial && yarn dev

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

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

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

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;

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

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:

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

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

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

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

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

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

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

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

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;

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

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

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

  1. How to Reset Formik Form
  2. Formik Tutorial: Only Send Modified or Changed Fields on onSubmit.

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