DEV Community

Alexander Dmitriev
Alexander Dmitriev

Posted on

How to create simple multi-step sign in with validation

Introduction

Let's say you need to create a multi-step login form like in gmail. You are using react and the global storage (redux, mobx) for development, and you want to isolate components from each other in order to reuse them in the future. Besides this, you need to add validation to each step. In this article I will show the simplest and most correct, in my opinion, solution. Complete solution you can check here

Dependencies

First of all, we need a library for processing the form, in my opinion the best solution is react-hook-forms (https://react-hook-form.com/), the site describes in great detail why this is an excellent solution, i will add on my own that this library has powerful functionality (validations, quick integrations, controller mechanism) and good documentation.
For validation we will use the yup library, it's very powerful and popular library
For global storage i will use little-state-machine, because it's very simple solution and built on a flux architecture. But you can use redux or mobx
To integrate yup validation schemas with react-hook-form you will also need @hookform/resolvers package.

Let's code

Project Structure

The example uses the following project structure

  • steps <- here will be all form steps
    • Congrats.js <- final step, if sign in is successed
    • Email.js <- First step, enter email to continue sign in
    • Password.js <- Second step, enter password to sign in
  • store
    • actions.js <- include all actions, in my case only one for update form state
    • index.js <- include app state, in my case only form state
  • App.js <- Main component, in my case include form logic
  • index
  • App.css <- App styles

About store

In the storage we will store information about the step of the form and email data. Let's add this information in store/index.js

const state = {
  step: "Email",
  email: ""
};

export default state;
Enter fullscreen mode Exit fullscreen mode

Now let's add an action to update the form in actions.js

const updateFormState = (state, payload) => {
  return {
    ...state,
    ...payload
  };
};

export default updateFormState;

Enter fullscreen mode Exit fullscreen mode

Let's add our storage to the application in index.js

import { StrictMode } from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { StateMachineProvider, createStore } from "little-state-machine";
import store from "./store";

// create out global form state
createStore(store);

const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <StateMachineProvider>
      <App />
    </StateMachineProvider>
  </StrictMode>,
  rootElement
);

Enter fullscreen mode Exit fullscreen mode

Base logic

The logic for switching the form, as well as its handlers, will be in App.js (for example only). We need to connect the store to the component in order to receive information about the form and update it.

import "./styles.css";
import { useStateMachine } from "little-state-machine";
import updateFormState from "./store/actions";
// Here we import form steps
import EmailStep from "./steps/Email";
import CongratsStep from "./steps/Congrats";
import PasswordStep from "./steps/Password";

export default function App() {
  // use hook for getting form state and actions
  const { state, actions } = useStateMachine({ updateFormState });
  // form handler for email step
  const emailFormHandle = ({ email }) => {
    actions.updateFormState({
      email: email,
      step: "Password"
    });
  };
  // form handler for password step
  const passwordFormHandle = ({ password }) => {
    actions.updateFormState({
      step: "Congrats"
    });
  };
  // sign out handler
  const signOutHandle = () => {
    actions.updateFormState({
      step: "Email"
    });
  };

  return (
    <div>
      {state.step === "Email" && (
        <EmailStep email={state.email} onSubmit={emailFormHandle} />
      )}
      {state.step === "Password" && (
        <PasswordStep onSubmit={passwordFormHandle} />
      )}
      {state.step === "Congrats" && (
        <CongratsStep email={state.email} onSignOut={signOutHandle} />
      )}
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode


javascript
Form step components are isolated from each other as much as possible, and can be reused in other parts of the application. All you need is only add default values, if they exists (for email step) and form handler function.

Steps

Email

The email entry step is the first step for user authorization. It is necessary to check the validity of the entered email, and remember it in case the user at the step with the password wants to go back and change it a little. This may seem very far-fetched, but when there are a lot of inputs in form, saving their state is very useful to save the user's time. Code with comments over here:

import { useForm } from "react-hook-form";
// import our validation library
import * as yup from "yup";
// import integration library
import { yupResolver } from "@hookform/resolvers/yup";
import cn from "classnames";

// validation schema
const Schema = yup.object().shape({
  // it says here that we want to check the input with the name email for the fact that the user will pass a string and this string matches email, you can change validation error message by changing text in email function argument
  email: yup.string().email("Enter valid email please")
});

const EmailStep = (props) => {
  // get form on Submit handler from parent component
  const { onSubmit, email } = props;
  // apply validations schema to react-hook-form form object
  const { errors, register, handleSubmit } = useForm({
    resolver: yupResolver(Schema),
    // if user input his email before we can paste it to input as default value
    defaultValues: {
      email
    }
  });

  //  you can check all validations errors in console
  console.log(errors);
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div className="form-group">
        <h2>Enter your email</h2>
      </div>
      <div className="form-group">
        {/* check validation errors */}
        {errors.email && (
          <h4 className="invalid-msg">{errors.email.message}</h4>
        )}
        <input
          // make input invalid if get email validation errors
          className={cn(errors.email && "input-invalid")}
          name="email"
          ref={register}
          placeholder="Your email"
        />
      </div>
      <div className="form-group">
        <button type="submit">Next</button>
      </div>
    </form>
  );
};

export default EmailStep;
Enter fullscreen mode Exit fullscreen mode

What you need to know:

  • Form validation will be apply after user click on submit button (Next button in my case), but you can change this behavior in form options
  • All validation errors are in the error object, which is generated by react-hook-form, the key is input name (email) and value is validation message (Enter valid email please)
  • You can use the default validation rules by react-hook-form form object, without any libraries, but yup is more powerful and flexible package.

Password step

The last step in user authorization. The password should be more that 6 symbols length and include Latin letters. The code is below:

import { useForm } from "react-hook-form";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import cn from "classnames";

const Schema = yup.object().shape({
  password: yup
    .string()
    .min(6, "Password is too short")
    .matches(/[a-zA-Z]/, "Password can only contain Latin letters.")
});

const PasswordStep = (props) => {
  const { onSubmit } = props;
  const { errors, register, handleSubmit } = useForm({
    resolver: yupResolver(Schema)
  });

  console.log(errors);
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div className="form-group">
        <h2>Enter your password</h2>
      </div>
      <div className="form-group">
        {errors.password && (
          <h4 className="invalid-msg">{errors.password.message}</h4>
        )}
        <input
          className={cn(errors.password && "input-invalid")}
          name="password"
          type="password"
          ref={register}
          placeholder="Your password"
        />
      </div>
      <div className="form-group">
        <button type="submit">Sign In</button>
      </div>
    </form>
  );
};

export default PasswordStep;
Enter fullscreen mode Exit fullscreen mode

Final step

And finally let's show user congrats message

const CongratsStep = (props) => {
  const { email, onSignOut } = props;

  return (
    <div className="form-group">
      <h2>
        Hello, {email}
        <button onClick={onSignOut}>Sign Out</button>
      </h2>
      <img src="https://i.giphy.com/6nuiJjOOQBBn2.gif" alt="" />
    </div>
  );
};

export default CongratsStep;
Enter fullscreen mode Exit fullscreen mode

Conclusion

That's all. We create isolated form steps, add default values for email value, add validation rules to every form step and use for this most powerful and popular packages (excluding little-state-machine).
If you interested i can show this examples with typescript, MUI and mobx or redux packages

P.S.

This is my first article, and english is not my native language, hope everything was clear and you had a pleasant time :) If you have problems with understanding the text (due to the fact that I do not know the language well), you can always look at my code, it says much more than any words

Discussion (2)

Collapse
jonhualde profile image
Jon

Hi Alexander, thank you for sharing this article.

I'm using React/Redux on a multi-step form as well. By reading your article, I was thinking on a use case.

In your example, the user can enter an email, and then can move to the 'sign up' section. But we cannot move back to change the email (eg: using the browser's backwards feature).

Let's assume we have that feature implemented using a router. (eg: pcz55.csb.app/email and pcz55.csb.app/signUp)

How would you pass the data from Redux to the component? Would you connect the parent component (App.jsx) to the store and pass the data to the child component as props? Or would you connect the Redux store directly to the email component?

Collapse
vishal2369 profile image
Vishal2369

It's really awesome.
I liked the terminal but I didn't find any hidden feature :( .