Skip to content

Instantly share code, notes, and snippets.

@Clarity-89
Last active October 5, 2022 14:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Clarity-89/73aba7ce1f714ad7172d32098c0e10bf to your computer and use it in GitHub Desktop.
Save Clarity-89/73aba7ce1f714ad7172d32098c0e10bf to your computer and use it in GitHub Desktop.
Building Multistep Forms With React Hook Form
// state.js
import React, { createContext, useContext, useState } from "react";
export const AppStateContext = createContext();
export function AppProvider({ children }) {
const value = useState({});
return (
<AppStateContext.Provider value={value}>
{children}
</AppStateContext.Provider>
);
}
export function useAppState() {
const context = useContext(AppStateContext);
if (!context) {
throw new Error("useAppState must be used within the AppProvider");
}
return context;
}
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { AppProvider } from "./state";
import { Contact } from "./Steps/Contact";
import { Education } from "./Steps/Education";
import { About } from "./Steps/About";
import { Confirm } from "./Steps/Confirm";
import "./styles.scss";
export const App = () => {
return (
<AppProvider>
<Router>
<Routes>
<Route path="/" element={<Contact />} />
<Route path="/education" element={<Education />} />
<Route path="/about" element={<About />} />
<Route path="/confirm" element={<Confirm />} />
</Routes>
</Router>
</AppProvider>
);
};
import React from "react";
export const Field = ({ children, label, error }) => {
const id = getChildId(children);
return (
<div className="col-sm-12 mb-3">
<label htmlFor={id} className="form-label">
{label}
</label>
{children}
{error && <small className="error">{error.message}</small>}
</div>
);
};
// Get id prop from a child element
export const getChildId = (children) => {
const child = React.Children.only(children);
if ("id" in child?.props) {
return child.props.id;
}
};
// Steps/Contact.js
import { useForm } from "react-hook-form";
import { useAppState } from "../state";
import { useNavigate } from "react-router-dom";
import { Button, Field, Form, Input } from "../Forms";
export const Contact = () => {
const [state, setState] = useAppState();
const {
handleSubmit,
register,
watch,
formState: { errors },
} = useForm({ defaultValues: state, mode: "onSubmit" });
const watchPassword = watch("password");
const navigate = useNavigate();
const saveData = (data) => {
setState({ ...state, ...data });
navigate("/education");
};
return (
<Form onSubmit={handleSubmit(saveData)}>
<fieldset>
<legend>Contact</legend>
<Field label="First name" error={errors?.firstName}>
<Input
{...register("firstName", { required: "First name is required" })}
id="first-name"
/>
</Field>
<Field label="Last name" error={errors?.lastName}>
<Input {...register("lastName")} id="last-name" />
</Field>
<Field label="Email" error={errors?.email}>
<Input
{...register("email", { required: "Email is required" })}
type="email"
id="email"
/>
</Field>
<Field label="Password" error={errors?.password}>
<Input
{...register("password", { required: "Password is required" })}
type="password"
id="password"
/>
</Field>
<Field label="Confirm password" error={errors?.confirmPassword}>
<Input
{...register("confirmPassword", {
required: "Confirm the password",
validate: (value) =>
value === watchPassword || "The passwords do not match",
})}
type="password"
id="password-confirm"
/>
</Field>
<Button>Next {">"}</Button>
</fieldset>
</Form>
);
};
// Steps/Education.js
import { useForm } from "react-hook-form";
import { useAppState } from "../state";
import { useNavigate } from "react-router-dom";
import { Button, Field, Form, Input } from "../Forms";
export const Education = () => {
const [state, setState] = useAppState();
const { handleSubmit, register } = useForm({ defaultValues: state });
const navigate = useNavigate();
const saveData = (data) => {
setState({ ...state, ...data });
navigate("/about");
};
return (
<Form onSubmit={handleSubmit(saveData)}>
<fieldset>
<legend>Education</legend>
<Field label="University">
<Input {...register("university")} id="university" />
</Field>
<Field label="Degree">
<Input {...register("degree")} id="degree" />
</Field>
<div className="button-row">
<Button variant="secondary" onClick={() => navigate("/")}>
{"<"} Previous
</Button>
<Button>Next {">"}</Button>
</div>
</fieldset>
</Form>
);
};
// Steps/About.js
import { useForm } from "react-hook-form";
import { useAppState } from "../state";
import { useNavigate } from "react-router-dom";
import { Button, Field, Form } from "../Forms";
export const About = () => {
const [state, setState] = useAppState();
const { handleSubmit, register } = useForm({ defaultValues: state });
const navigate = useNavigate();
const saveData = (data) => {
setState({ ...state, ...data });
navigate("/confirm");
};
return (
<Form onSubmit={handleSubmit(saveData)}>
<fieldset>
<legend>About</legend>
<Field label="About me">
<textarea
{...register("about")}
id="about"
className="form-control"
/>
</Field>
<div className="button-row">
<Button variant="secondary" onClick={() => navigate("/education")}>
{"<"} Previous
</Button>
<Button>Next {">"}</Button>
</div>
</fieldset>
</Form>
);
};
//Section.js
import { useNavigate } from "react-router-dom";
import { Button } from "./Button";
export const Section = ({ title, children, url }) => {
const navigate = useNavigate();
return (
<div className="section mb-4">
<div className="title-row mb-4">
<h4>{title}</h4>
<Button type="button" variant="secondary" onClick={() => navigate(url)}>
Edit
</Button>
</div>
<div className="content">{children}</div>
</div>
);
};
export const SectionRow = ({ children }) => {
return <div className="section-row">{children}</div>;
};
// Steps/Confirm.js
import { useAppState } from "../state";
import { useForm } from "react-hook-form";
import { Button, Form, Section, SectionRow } from "../Forms";
export const Confirm = () => {
const [state] = useAppState();
const { handleSubmit } = useForm({ defaultValues: state });
const submitData = (data) => {
console.info(data);
// Submit data to the server
};
return (
<Form onSubmit={handleSubmit(submitData)}>
<h1 className="mb-4">Confirm</h1>
<Section title="Personal info" url="/">
<SectionRow>
<div>First name</div>
<div>{state.firstName}</div>
</SectionRow>
<SectionRow>
<div>Last name</div>
<div>{state.lastName}</div>
</SectionRow>
<SectionRow>
<div>Email</div>
<div>{state.email}</div>
</SectionRow>
</Section>
<Section title="Education" url="/education">
<SectionRow>
<div>University</div>
<div>{state.university}</div>
</SectionRow>
<SectionRow>
<div>Degree</div>
<div>{state.degree}</div>
</SectionRow>
</Section>
<Section title="About" url="/about">
<SectionRow>
<div>About me</div>
<div>{state.about}</div>
</SectionRow>
</Section>
<div className="clo-md-12 d-flex justify-content-start">
<Button>Submit</Button>
</div>
</Form>
);
};
import { useLocation } from "react-router-dom";
export const Stepper = () => {
const location = useLocation();
const getLinkClass = (path) => {
return (
"nav-link disabled " + (path === location.pathname ? "active" : undefined)
);
};
return (
<nav className="stepper navbar navbar-expand-lg">
<div className="collapse navbar-collapse">
<ol className="navbar-nav">
<li className="step nav-item">
<span className={getLinkClass("/")}>Contact</span>
</li>
<li className="step nav-item">
<span className={getLinkClass("/education")}>Education</span>
</li>
<li className="step nav-item">
<span className={getLinkClass("/about")}>About</span>
</li>
<li className="step nav-item">
<span className={getLinkClass("/confirm")}>Confirm</span>
</li>
</ol>
</div>
</nav>
);
};
export const App = () => {
return (
<AppProvider>
<Router>
<Stepper />
<Routes>
<Route path="/" element={<Contact />} />
<Route path="/education" element={<Education />} />
<Route path="/about" element={<About />} />
<Route path="/confirm" element={<Confirm />} />
</Routes>
</Router>
</AppProvider>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment