DEV Community

Cover image for Building the Login Page in React with Reusable Components
Sandhya-Allgandhuala
Sandhya-Allgandhuala

Posted on

Building the Login Page in React with Reusable Components

Introduction

Building a login page for the Smart Budget Tracker app using the reusable components we discussed in our previous post — please check that post out before following along here.
To build this component, we need two text inputs (for email/username and password), a login button, and API calls to send the authentication request and receive the authorization response.


Defining the Login Page Interface and Setting Up Required Hooks

Below is LoginForm, the required fields for the login page — both fields accept strings.

We define the states required for this form:

  • loginForm state with initial values as empty strings
  • loading state with an initial value of false
  • error state with an initial value of an empty string
interface LoginForm {
    email: string;
    password: string;
}

function Login() {
    const [loginForm, setloginForm] = useState<LoginForm>({
        email: '',
        password: '',
    });
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState('');
Enter fullscreen mode Exit fullscreen mode

Reusable Components to Build the Login Page

    return (
        <div className="flex">
            <LoginSideBar />
            <main className="w-1/2  bg-[#1a7a5e] border-r flex flex-col px-6 py-6 sticky top-0 h-screen justify-center overflow-y-auto">
                <div className='items-center justify-center text-center'>
                    <h1 className='text-xl'>Welcome Back</h1>
                    <p className='text-sm'>Sign in to your account to continue</p>
                </div>
                <form className="auth-form" onSubmit={handleSubmit}>
                    <TextInput
                        label="Email Address"
                        name="email"
                        type="email"
                        placeholder="Sample@SmartBudget.com"
                        value={loginForm.email}
                        onChange={handleChange}
                        error={error}
                        disabled={loading}
                        leftIcon={<img src={emailIcon} alt="email" width={18} height={18} />}
                    />
                    <TextInput
                        label="Password"
                        name="password"
                        type="password"
                        placeholder="********"
                        value={loginForm.password}
                        onChange={handleChange}
                        leftIcon={<img src={lockIcon} alt="lock" width={18} height={18} />}
                        rightAction="Forgot password?"
                        error={error}
                        disabled={loading}
                        maxLength={12}
                        hint="Only 12 characters are allowed"
                    />
                    <Button
                        label="Log in"
                        type="submit"
                        variant="primary"
                        fullWidth
                        loading={loading}
                    />
                </form>
                <p>Don't have an account? <Button label="Register Free" href="/Register" className="btn-base btn-ghost btn-md" /></p>
            </main>
        </div>
    );
Enter fullscreen mode Exit fullscreen mode

By keeping TextInputand Buttongeneric and fully controlled through props, we avoid duplicating markup and styling logic across the form. The parent component Loginowns the state, validation, and submit logic, while the child components simply render based on the props they receive — this keeps the login page lightweight and lets us reuse the same components anywhere else a form input or button is needed

Both inputs share the same error prop, sourced from a single error state in the parent component. This means any authentication error — such as "Invalid credentials" — is displayed under both fields rather than being tied to one specific input. If you need field-level validation later (e.g., "Email is required" vs. "Password is too short"), you'd extend this into an object shape like { email: string; password: string } and pass the relevant key to each input.

Notice that Button is used two different ways in this form: once as a type="submit" trigger inside the form, and once as a navigational link (via the href prop) for the "Register Free" action. This is another example of a single reusable component adapting its behavior based on the props it's given, rather than needing separate Button and Link components for each case.


HandleChange and HandleSubmit Functions

setLoginForm({ ...loginForm, [e.target.name]: e.target.value });
This line runs every time the user types something in the email or password box.

e.target.name tells us which input the user is typing in — either "email" or "password".
e.target.value is what the user typed.
...loginForm copies the current state (both email and password) so we don't lose the other field's value.
[e.target.name]: e.target.value then updates just the one field that changed.

This way, one function can handle changes for both inputs, instead of writing separate functions for email and password.
Example: If the user types "a" in the email box, the state becomes { email: "a", password: "" } — the password stays the same, only email updates.

 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setLoginForm({ ...loginForm, [e.target.name]: e.target.value });
    };

    const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
        e.preventDefault();
        setError('');

        if (loginForm.email.length === 0) {
            setError('Please fill out this field');
            return;
        }

         if (loginForm.password.length === 0) {
            setError('Please fill out this field');
            return;
        }

        setLoading(true);
        try {
            const result = await loginUser({ email: loginForm.email, password: loginForm.password });
            localStorage.setItem('token', result.token);
            localStorage.setItem('email', result.email);
            localStorage.setItem('userName', result.userName);
            navigate('/UserDashboard');
        } catch (err) {
            setError(err instanceof Error ? err.message : 'Login failed.');
        } finally {
            setLoading(false);
        }
    };
Enter fullscreen mode Exit fullscreen mode

The handleSubmit function is responsible for five things:

  • Resetting the error state to an empty string and checking that both form fields have values — if not, it sets the error state.
  • Setting the loading state to true before sending the POST API call.
  • Triggering the API call and storing the result in the result variable.
  • Saving the returned data (token, email, and username) to local storage.
  • Navigating the user to the dashboard.

What's Next

In my next post, I'll cover how I built the API call for login and added Zod validation for runtime checks.

If this helped you, consider dropping a like or comment — feedback is always welcome!

Top comments (0)