Building authentication from scratch can feel intimidating for many developers, especially when just starting with React. The challenge isn’t just rendering a form—it’s how to structure and manage the authentication state, handle validations, prevent unwanted navigation, and ensure a good user experience. Creating a login and registration form that is both functional and clean, while integrating React best practices, is more involved than it initially seems.
Form Structure
The first decision I made was whether to separate login and registration forms into distinct components or use a toggle system within a single component. I chose the latter because it gave me a simpler UI and reduced code duplication for handling inputs.
I started with a basic form layout, using a useState
hook to track whether the user is in login or register mode. This allowed me to toggle between two views with one shared input handler, improving maintainability.
Input Handling
Rather than writing a separate onChange
for each input field, I created a shared handler that updates the form state dynamically. Here’s how I set it up:
const [formData, setFormData] = useState({ email: '', password: '', name: '' });
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
This pattern saved me from creating multiple pieces of state, keeping the form logic focused and reusable.
Validation Logic
Early on, I made the mistake of trying to validate the form after submission only. That meant users wouldn’t see any errors until they clicked “submit,” which felt jarring. I moved to real-time validation instead using basic checks inside handleChange
.
To keep things fast and user-friendly, I avoided full-blown validation libraries and used inline validation such as:
if (name === 'email' && !value.includes('@')) {
// simple feedback
}
For more serious projects, libraries like Yup or Zod with React Hook Form can be integrated for schema-based validation, but for a simple login/register, native validation and some conditionals often do the job well.
Authentication State
To simulate authentication, I stored a loggedIn
boolean using useState
. It wasn’t connected to a backend, but for real apps, I’d replace this with a proper authentication token saved in localStorage
or context.
One mistake I ran into was not resetting the form after a successful login. So I added a resetForm()
utility that clears the form state on login/register.
const resetForm = () => {
setFormData({ email: '', password: '', name: '' });
};
This seems minor, but it’s the kind of polish that improves user experience.
Once authenticated, I used conditional rendering to display either the login/register form or the welcome message. In more complex setups, I would use useNavigate
from React Router, but here’s a simple version:
return loggedIn ? <Welcome /> : <AuthForm />
This technique kept the logic clear. In real-world scenarios, redirecting users with route guards is better handled with protected routes.
Real-time Feedback
A useful tip I learned was to give users immediate visual feedback. I added an error message area below each field and a general message like “Logging in…” to simulate latency. This is especially helpful for asynchronous operations like network requests.
Sample Form UI
Here’s how the table layout looked for better structure in the UI:
Field | Validation | Required | Applies To |
---|---|---|---|
Name | Minimum 3 characters | Yes | Register |
Must include “@” | Yes | Both | |
Password | Minimum 6 characters | Yes | Both |
This simple visual guide helped me validate each field without forgetting edge cases.
Personal Tips
- Avoid storing passwords in localStorage, even for mock setups—use in-memory state or mock APIs.
- Use React Fragment (
<>
) instead of extra<div>
s to keep DOM clean. - Abstract input fields into a reusable
<Input />
component when scaling beyond two or three fields. - Don’t forget mobile-friendly styling, even in small projects. It adds more polish than expected.
Mistakes to Avoid
One major mistake I made early was not disabling the submit button during processing. This led to double submissions and weird state issues. So I added a simple isSubmitting
flag to prevent this.
Also, skipping accessibility features like label bindings or aria-*
tags made the form less usable for screen readers. While I was focused on JavaScript logic, these minor details significantly impact the user experience.
Conclusion
Building a simple login and registration form in React isn’t about just rendering inputs. It’s about creating smooth transitions between states, real-time feedback, proper error handling, and clean code structure. Small improvements—like managing button states or adding basic validation—add up to a smoother user experience. For more refined UI work, like modern login pages, I’ve explored form design inspirations that helped me understand how much impact design has even in basic projects.
When I stepped back to review what I had built, I realized a functional authentication experience in React is less about complexity and more about thoughtful, incremental decisions that prioritize the user’s journey.
For anyone looking to go further, combining this form with protected routing logic and form libraries like Formik or React Hook Form opens the door to real-world production readiness.