Building a Seamless Login Experience in React with FormProvider and Controller
Creating a seamless login experience is crucial for any web application. In this blog post, we'll focus on using react-hook-form
's FormProvider
and Controller
to set up a robust login form in a React application. These components help manage form state and validations more effectively. We will integrate them with Redux, RTK Query, and Ant Design to handle user authentication efficiently.
Prerequisites
Before diving into the implementation, make sure you have the following:
React and Redux Setup: Ensure you have a React application with Redux Toolkit and RTK Query installed.
Ant Design: For UI components like Button and Row.
React Hook Form: For handling form state and validation.
Why Use FormProvider and Controller?
FormProvider: This component allows you to share the same form context across your application. It makes it easier to manage complex forms and nested inputs.
Controller: This component connects your input components to the
react-hook-form
's state and validation. It simplifies the process of integrating third-party components (like Ant Design) withreact-hook-form
.
Step 1: Setting Up the Login Component
We start by creating a login component that uses react-hook-form
for managing form state and RTK Query for handling API requests. We'll use Ant Design for styling and layout.
import { Button, Row } from "antd";
import { FieldValues } from "react-hook-form";
import { useDispatch } from "react-redux";
import { setUser, TUser } from "../redux/features/auth/authSlice";
import { verifyToken } from "../utils/verifyToken";
import { useLoginMutation } from "../redux/features/auth/authApi";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import PHForm from "../components/form/PHForm";
import PHInput from "../components/form/PHInput";
const Login = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const [login] = useLoginMutation();
const onSubmit = async (data: FieldValues) => {
const toastId = toast.loading("Logging in...");
try {
const userInfo = {
id: data.userId,
password: data.password,
};
const res = await login(userInfo).unwrap();
const user = verifyToken(res.data.accessToken) as TUser;
dispatch(setUser({ user, token: res.data.accessToken }));
toast.success("Login successful", { id: toastId, duration: 2000 });
navigate(`/${user.role}/dashboard`);
} catch (error) {
toast.error("Something went wrong.", { id: toastId, duration: 2000 });
}
};
const defaultValues = {
userId: "A-0001",
password: "admin123",
};
return (
<Row justify="center" align="middle" style={{ height: "100vh" }}>
<PHForm onSubmit={onSubmit} defaultValues={defaultValues}>
<PHInput type="text" name="userId" label="ID" />
<PHInput type="password" name="password" label="Password" />
<Button htmlType="submit">Login</Button>
</PHForm>
</Row>
);
};
export default Login;
In this component, we use react-hook-form
to manage the form state and handle form submission. The onSubmit
function calls the login API and handles the response. If the login is successful, it dispatches the user information to the Redux store and navigates to the user's dashboard. If there's an error, it shows an error toast notification.
Step 2: Creating Reusable Form and Input Components with FormProvider and Controller
Next, we create reusable form and input components using react-hook-form
's FormProvider
and Controller
.
Creating the Form Component
import { ReactNode } from "react";
import { FieldValues, FormProvider, SubmitHandler, useForm } from "react-hook-form";
type TDefaultValues = {
userId: string;
password: string;
};
type TPHFormProps = {
onSubmit: SubmitHandler<FieldValues>;
children: ReactNode;
defaultValues?: TDefaultValues;
};
const PHForm = ({ onSubmit, children, defaultValues }: TPHFormProps) => {
const methods = useForm({ defaultValues });
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>{children}</form>
</FormProvider>
);
};
export default PHForm;
The PHForm
component wraps the react-hook-form
's FormProvider
to provide form context to its children. It takes onSubmit
, children
, and defaultValues
as props.
Creating the Input Component
import { Input } from "antd";
import { Controller } from "react-hook-form";
type TPHInputProps = {
type: string;
name: string;
label: string;
};
const PHInput = ({ type, name, label }: TPHInputProps) => {
return (
<div style={{ marginBottom: "20px" }}>
<label htmlFor={name}>{label}:</label>
<Controller
name={name}
render={({ field }) => (
<Input type={type} id={name} {...field} />
)}
/>
</div>
);
};
export default PHInput;
The PHInput
component uses react-hook-form
's Controller
to connect the input field with the form state. It takes type
, name
, and label
as props and renders an Ant Design Input
component.
Step 3: Implementing Authentication Logic
The core of our authentication logic lies in the onSubmit
function of the Login
component. This function handles form submission makes an API call to log in the user and processes the response.
API Call: We use the
useLoginMutation
hook generated by RTK Query to make the login request.Token Verification: After receiving the token, we verify it using a custom
verifyToken
function.State Management: We dispatch the user information and token to the Redux store.
Navigation: If login is successful, we navigate the user to their respective dashboard.
Conclusion
Using react-hook-form
's FormProvider
and Controller
allows us to create reusable and maintainable form components. These components provide a seamless and user-friendly login experience in React applications. By integrating Redux and RTK Query, we manage state and handle API requests efficiently, ensuring our application remains secure and responsive.
Implementing these techniques will not only enhance your application's security but also improve user satisfaction by providing a reliable and responsive login experience.