Two-step login form in React

Google has a unique login UX that completes the logic flow in two steps, First, where the email is verified, and second where the password verification is done.

In this article, we will see how we can implement the same in Reactjs using Syncfusion’s react components library.

As the login flow suggests, we will have to complete the process in two steps, thus we will have to conditionally render two different components and get their input and validate them before moving to the next step.

Keeping this in mind let us create the structure of the component.

import { useState } from "react";

const LoginForm = () => {
   const [track, setTrack] = useState(0);

   // verify the email first and then the password
   return track === 0 ? <Email /> : <Password />;

}

export default LoginForm;

Let us create the Email and the Password components separately and then combine them.

Email component

Getting the UI reference from the Google login, the email component will have an input box and the next button that will verify the email and update the state to move to the next step.

We will be creating the same, for the Input box we will be using the TextBoxComponent from the @syncfusion/ej2-react-inputs, and for the button, we will be using ButtonComponent from the @syncfusion/ej2-react-buttons. You can use any component library of your choice.

There are stylesheets available for these components which help to use all variations of it.

import "@syncfusion/ej2-base/styles/material.css";
import "@syncfusion/ej2-inputs/styles/material.css";
import "@syncfusion/ej2-buttons/styles/material.css";

TextBoxComponent provides an excellent wrapper around the input elements allowing better control over them with style as well as value. We can pass the type parameter to make it render the desired input field.

The same goes for the ButtonComponent it comes in multiple variations as well as can be easily extended making it great to work with.

<>
 <TextBoxComponent
   type="email"
   value={email}
   placeholder="Email"
   floatLabelType="Auto"
   input={({ value }) => setEmail(value)}
   cssClass="e-outline"
 />
 <div className="buttonWrapper">
   <ButtonComponent
     type="submit"
     cssClass="e-info"
     style={{ fontSize: "18px", padding: "10px 20px" }}
     onClick={verifyEmail}
   >
     Next
   </ButtonComponent>
 </div>{" "}
</>;

We want the email input box to be a controlled component and thus we will have to maintain its state.

For the button, we are listening to the onClick event and on its click, we will perform the asynchronous operations to verify the email and then navigate to the next step.

const [email, setEmail] = useState("");

const verifyEmail = async () => {
 // async operations

 // validate email
 if (email) {
   setTrack(1);
 }
};

Here I am just changing the track if there is some value in the email state, but you can perform all sorts of operations you like.

Password component

As we have created the email component the same way we can use TextBoxComponent and ButtonComponent to create the Password component and on its button click, we can perform the final check to authenticate the user.

const [password, setPassword] = useState("");

const handleSubmit = async () => {
 // async operations
};

<>
 <TextBoxComponent
   type="password"
   value={password}
   placeholder="Password"
   floatLabelType="Auto"
   input={({ value }) => setPassword(value)}
   cssClass="e-outline"
   key="2"
 />
 <div className="buttonWrapper">
   <ButtonComponent
     type="submit"
     cssClass="e-danger"
     onClick={() => setTrack(0)}
     style={{ fontSize: "18px", padding: "10px 20px" }}
   >
     Change Email
   </ButtonComponent>{" "}
   <ButtonComponent
     type="submit"
     cssClass="e-success"
     onClick={handleSubmit}
     style={{ fontSize: "18px", padding: "10px 20px" }}
   >
     Submit
   </ButtonComponent>
 </div>
</>;

Here if you see I have added two buttons, one to navigate back to the email verification part and the other for final verification.

Putting all pieces together.

import { useState } from "react";
import { TextBoxComponent } from "@syncfusion/ej2-react-inputs";
import { ButtonComponent } from "@syncfusion/ej2-react-buttons";
import "@syncfusion/ej2-base/styles/material.css";
import "@syncfusion/ej2-inputs/styles/material.css";
import "@syncfusion/ej2-buttons/styles/material.css";
import "./App.css";

const LoginForm = () => {
 const [track, setTrack] = useState(0);
 const [email, setEmail] = useState("");
 const [password, setPassword] = useState("");

 const verifyEmail = async () => {
   // async operations
   // validate email
   if (email) {
     setTrack(1);
   }
 };

 const handleSubmit = async () => {
   // async operations
 };

 return (
   <div className="box">
     <div style={{ textAlign: "center" }}>
       <p style={{ fontSize: "1.5em" }}>Sign In</p>
       <p>to continue to Gmail.</p>
     </div>
     {track === 0 ? (
       <>
         <TextBoxComponent
           type="email"
           value={email}
           placeholder="Email"
           floatLabelType="Auto"
           input={({ value }) => setEmail(value)}
           cssClass="e-outline"
         />
         <div className="buttonWrapper">
           <ButtonComponent
             type="submit"
             cssClass="e-info"
             style={{ fontSize: "18px", padding: "10px 20px" }}
             onClick={verifyEmail}
           >
             Next
           </ButtonComponent>
         </div>{" "}
       </>
     ) : (
       <>
         <TextBoxComponent
           type="password"
           value={password}
           placeholder="Password"
           floatLabelType="Auto"
           input={({ value }) => setPassword(value)}
           cssClass="e-outline"
           key="2"
         />
         <div className="buttonWrapper">
           <ButtonComponent
             type="submit"
             cssClass="e-danger"
             onClick={() => setTrack(0)}
             style={{ fontSize: "18px", padding: "10px 20px" }}
           >
             Change Email
           </ButtonComponent>{" "}
           <ButtonComponent
             type="submit"
             cssClass="e-success"
             onClick={handleSubmit}
             style={{ fontSize: "18px", padding: "10px 20px" }}
           >
             Submit
           </ButtonComponent>
         </div>
       </>
     )}
   </div>
 );
};

export default LoginForm;

If you are wondering why I haven’t separated the Email and Password components into two different functions like this.

import { useState } from "react";
import { TextBoxComponent } from "@syncfusion/ej2-react-inputs";
import { ButtonComponent } from "@syncfusion/ej2-react-buttons";
import "@syncfusion/ej2-base/styles/material.css";
import "@syncfusion/ej2-inputs/styles/material.css";
import "@syncfusion/ej2-buttons/styles/material.css";
import "./App.css";

const LoginForm = () => {
 const [track, setTrack] = useState(0);
 const [email, setEmail] = useState("");
 const [password, setPassword] = useState("");

 const verifyEmail = async () => {
   // async operations
   // validate email
   if (email) {
     setTrack(1);
   }
 };

 const handleSubmit = async () => {
   // async operations
 };

 const Email = () => (
   <>
     <TextBoxComponent
       type="email"
       value={email}
       placeholder="Email"
       floatLabelType="Auto"
       input={({ value }) => setEmail(value)}
       cssClass="e-outline"
     />
     <div className="buttonWrapper">
       <ButtonComponent
         type="submit"
         cssClass="e-info"
         style={{ fontSize: "18px", padding: "10px 20px" }}
         onClick={verifyEmail}
       >
         Next
       </ButtonComponent>
     </div>{" "}
   </>
 );

 const Password = () => (
   <>
     <TextBoxComponent
       type="password"
       value={password}
       placeholder="Password"
       floatLabelType="Auto"
       input={({ value }) => setPassword(value)}
       cssClass="e-outline"
       key="2"
     />
     <div className="buttonWrapper">
       <ButtonComponent
         type="submit"
         cssClass="e-danger"
         onClick={() => setTrack(0)}
         style={{ fontSize: "18px", padding: "10px 20px" }}
       >
         Change Email
       </ButtonComponent>{" "}
       <ButtonComponent
         type="submit"
         cssClass="e-success"
         onClick={handleSubmit}
         style={{ fontSize: "18px", padding: "10px 20px" }}
       >
         Submit
       </ButtonComponent>
     </div>
   </>
 );

 // verify the email first and then the password
 return track === 0 ? <Email /> : <Password />;
};

export default LoginForm;

Well because if I wrap them in a function and then use the global state, it creates a bug, and because of this, the input component loses its focus every time we type something because once the state is updated the component re-renders and because the input field is wrapped inside a function, it is treated as functional component thus it loses it focus.

To solve this we can maintain the state in each component but that will increase complexity as we will have to do the prop drill down or use a global state.