Sitemap

REACT.JS - Part 5

8 min readAug 20, 2026

--

Reusable Components, Code Splitting, Forms & Validation

Project Structure

student-manager/
│
├── src/
│ │
│ ├── components/
│ │ ├── Button.jsx
│ │ ├── Input.jsx
│ │ ├── StudentCard.jsx
│ │ └── Loader.jsx
│ │
│ ├── pages/
│ │ ├── Home.jsx
│ │ ├── Students.jsx
│ │ ├── AddStudent.jsx
│ │ └── Dashboard.jsx
│ │
│ ├── schemas/
│ │ └── studentSchema.js
│ │
│ ├── services/
│ │ └── studentService.js
│ │
│ ├── hooks/
│ │ └── useStudents.js
│ │
│ ├── App.jsx
│ └── main.jsx
│
├── package.json
└── index.html

The important thing is that each folder has a responsibility.

Reusable Components

Imagine we need several buttons.

Without reusable components, you might write:

<button className="btn">Add Student</button>
<button className="btn">Save Student</button>
<button className="btn">Update Student</button>

If you later change the design, you have to change multiple places.

Instead, create:

components/
└── Button.jsx

Button.jsx

function Button({ children, type = "button", onClick }) {
return (
<button
type={type}
onClick={onClick}
className="bg-blue-600 text-white px-4 py-2 rounded"
>
{children}
</button>
);
}
export default Button;

Now anywhere in your application:

import Button from "../components/Button";
function AddStudent() {
return (
<div>
<h1>Add Student</h1>
<Button>
Add Student
</Button>
</div>
);
}
export default AddStudent;

You can reuse it:

<Button>Save</Button>
<Button>Update</Button>
<Button>Register</Button>

Understanding children

This:

<Button>
Add Student
</Button>

passes:

Add Student

into the children prop.

Therefore:

function Button({ children }) {
return (
<button>
{children}
</button>
);
}

is essentially saying:

Whatever is placed between <Button> and </Button>, put it inside the button.

You can even put more complex content:

<Button>
Save Student
</Button>

or:

<Button>
Delete Student
</Button>

Reusable Input Component

Instead of repeating this:

<input />

throughout the application, create:

components/
└── Input.jsx
function Input({
label,
type = "text",
placeholder,
error,
...props
}) {
return (
<div className="mb-4">
<label className="block mb-1">
{label}
</label>
<input
type={type}
placeholder={placeholder}
{...props}
className="border p-2 w-full rounded"
/>
{error && (
<p className="text-red-500 text-sm">
{error}
</p>
)}
</div>
);
}
export default Input;

Now:

<Input
label="Student Name"
placeholder="Enter student name"
/>

Another:

<Input
label="Email"
type="email"
placeholder="Enter email"
/>

Another:

<Input
label="Age"
type="number"
placeholder="Enter age"
/>

This is the power of reusable components.

Folder Structure

Now let’s understand why we created different folders.

components

Contains reusable UI:

components/
├── Button.jsx
├── Input.jsx
├── StudentCard.jsx
└── Loader.jsx

These components can be used by multiple pages.

pages

Contains complete screens:

pages/
├── Home.jsx
├── Students.jsx
├── AddStudent.jsx
└── Dashboard.jsx

For example:

function Home() {
return (
<div>
<h1>Student Manager</h1>
<p>Manage your students.</p>
</div>
);
}
export default Home;

schemas

Contains validation rules.

schemas/
└── studentSchema.js

Instead of putting validation logic everywhere, we keep it here.

services

Contains API-related logic.

services/
└── studentService.js

For example:

export async function getStudents() {
const response = await fetch(
"https://example.com/api/students"
);
return response.json();
}

Your component doesn’t need to know how the API works.

Code Splitting

Imagine our application has:

Home
Students
Dashboard
Add Student
Reports
Settings

The user initially visits:

/

Why should the browser immediately download all the JavaScript for:

Dashboard
Reports
Settings

when the user hasn’t visited them?

Code splitting solves this.

Normal Import

Normally:

import Dashboard from "./pages/Dashboard";

The component is included in the application’s JavaScript bundle.

Lazy Loading

Instead:

import { lazy } from "react";
const Dashboard = lazy(
() => import("./pages/Dashboard")
);

Now React can load Dashboard separately when it is needed.

This is called lazy loading.

Suspense

There’s a problem.

If Dashboard hasn’t loaded yet, what should React display?

We use:

Suspense

Full example:

import {
lazy,
Suspense
} from "react";
const Dashboard = lazy(
() => import("./pages/Dashboard")
);
function App() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Dashboard />
</Suspense>
);
}
export default App;

The process is:

User requests Dashboard
↓
React starts downloading Dashboard
↓
Dashboard isn't ready
↓
Suspense displays "Loading..."
↓
Dashboard finishes loading
↓
Dashboard appears

Lazy Loading with React Router

This is where lazy loading becomes extremely useful.

Our App.jsx:

import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";

const Home = lazy(() => import("./pages/Home"));
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Profile = lazy(() => import("./pages/Profile"));

function App() {
return (
<BrowserRouter>
<Suspense fallback={<p>Loading...</p>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}

Now every page can be loaded independently.

Forms

Now let’s build our Add Student page.

The form needs:

Name
Email
Age
Course
Submit

Start with normal React state.

import { useState } from "react";

function AddStudent() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [age, setAge] = useState("");
const [course, setCourse] = useState("");
function handleSubmit(event) {
event.preventDefault();
console.log({
name,
email,
age,
course
});
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) =>
setName(e.target.value)
}
placeholder="Name"
/>
<input
value={email}
onChange={(e) =>
setEmail(e.target.value)
}

This works.

Get Tochukwu Mezue’s stories in your inbox

Join Medium for free to get updates from this writer.

But imagine 20 form fields.

You could end up with:

const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [age, setAge] = useState("");
const [course, setCourse] = useState("");
const [phone, setPhone] = useState("");
const [address, setAddress] = useState("");
const [country, setCountry] = useState("");

That’s where React Hook Form becomes useful.

React Hook Form

Install it:

npm install react-hook-form

Then:

import { useForm } from "react-hook-form";
function AddStudent() {
const {
register,
handleSubmit,
formState: {
errors
}
} = useForm();
function onSubmit(data) {
console.log(data);
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("name")}
placeholder="Name"
/>
<input
{...register("email")}
placeholder="Email"
/>
<input
{...register("age")}
placeholder="Age"
/>
<input
{...register("course")}
placeholder="Course"
/>
<button type="submit">
Add Student
</button>
</form>
);
}
export default AddStudent;

Notice something important.

We no longer need:

useState("")

for every field.

React Hook Form manages the form values for us.

What Does register() Do?

When we write:

<input {...register("name")} />

we’re telling React Hook Form:

This input belongs to the form and its name is name.

Another:

<input {...register("email")} />

means:

Track this field as email.

When submitted:

function onSubmit(data) {
console.log(data);
}

You might get:

{
name: "John",
email: "john@gmail.com",
age: "22",
course: "Computer Science"
}

Form Validation

Now we need to make sure users don’t submit invalid data.

For example:

Name → required
Email → required + valid email
Age → required
Course → required

React Hook Form can handle simple validation.

<input
{...register("name", {
required: "Name is required"
})}
/>

Then:

{errors.name && (<p>{errors.name.message}</p>
)}

For email:

<input
type="email"
{...register("email", {
required: "Email is required"
})}
/>

More Advanced Validation

Let’s validate the age:

<input
type="number"
{...register("age", {
required: "Age is required",
min: {
value: 16,
message: "Student must be at least 16"
},
max: {
value: 100,
message: "Enter a valid age"
}
})}
/>

Now React Hook Form automatically checks the rules.

Building a Complete Form

Let’s combine our reusable Input component with React Hook Form.

components/Input.jsx

function Input({
label,
type = "text",
error,
...props
}) {
return (
<div className="mb-4">
<label>
{label}
</label>
<input
type={type}
{...props}
className="border p-2 w-full"
/>
{error && (
<p className="text-red-500">
{error}
</p>
)}
</div>
);
}
export default Input;

pages/AddStudent.jsx

import { useForm } from "react-hook-form";
import Input from "../components/Input";
import Button from "../components/Button";
function AddStudent() {
const {
register,
handleSubmit,
formState: {
errors
}
} = useForm();
function onSubmit(data) {
console.log(data);
}
return (
<div className="max-w-lg mx-auto">
<h1>
Add Student
</h1>
<form onSubmit={handleSubmit(onSubmit)}>
<Input
label="Name"
placeholder="Enter student name"
{...register("name", {
required: "Name is required"
})}
error={errors.name?.message}
/>
<Input
label="Email"
type="email"
placeholder="Enter email"
{...register("email", {
required: "Email is required"
})}
error={errors.email?.message}
/>
<Input
label="Age"
type="number"
placeholder="Enter age"
{...register("age", {
required: "Age is required"
})}
error={errors.age?.message}
/>
<Input
label="Course"
placeholder="Enter course"
{...register("course", {
required: "Course is required"
})}
error={errors.course?.message}
/>
<Button type="submit">
Add Student
</Button>
</form>
</div>
);
}
export default AddStudent;

Now we have:

Reusable Input
↓
React Hook Form
↓
Validation
↓
Reusable Button
↓
Submit

Yup Schema Validation

The previous approach puts validation inside the form:

register("name", {
required: "Name is required"
})

For a large application, you might want all validation rules in one place.

Create:

schemas/
└── studentSchema.js

Install Yup:

npm install yup

Then:

studentSchema.js

import * as yup from "yup";
export const studentSchema = yup.object({
name: yup
.string()
.required("Name is required")
.min(2, "Name must be at least 2 characters"),
email: yup
.string()
.email("Enter a valid email")
.required("Email is required"),
age: yup
.number()
.required("Age is required")
.min(16, "Minimum age is 16"),
course: yup
.string()
.required("Course is required")
});

Now our validation rules are centralized.

Connecting Yup to React Hook Form

Install:

npm install @hookform/resolvers

Then:

import { useForm } from "react-hook-form";
import {
yupResolver
} from "@hookform/resolvers/yup";
import {
studentSchema
} from "../schemas/studentSchema";

Configure the form:

const {
register,
handleSubmit,
formState: {
errors
}
} = useForm({
resolver: yupResolver(studentSchema)
});

Now the form knows about the Yup schema.

Your input becomes much cleaner:

<Input
label="Name"
{...register("name")}
error={errors.name?.message}
/>

No validation rules are needed inside the input.

The schema handles them.

Zod Instead of Yup

Zod works in a very similar way.

Install:

npm install zod

Create:

schemas/
└── studentSchema.js
import { z } from "zod";
export const studentSchema = z.object({
name: z
.string()
.min(2, "Name must be at least 2 characters"),
email: z
.string()
.email("Enter a valid email"),
age: z
.coerce
.number()
.min(16, "Minimum age is 16"),
course: z
.string()
.min(1, "Course is required")
});

Then:

import {zodResolver
} from "@hookform/resolvers/zod";

And:

const form = useForm({resolver: zodResolver(studentSchema)
});

Complete Architecture

At this point, our project looks like this:

src/
│
├── components/
│ │
│ ├── Button.jsx
│ ├── Input.jsx
│ ├── StudentCard.jsx
│ └── Loader.jsx
│
├── pages/
│ │
│ ├── Home.jsx
│ ├── Students.jsx
│ ├── AddStudent.jsx
│ └── Dashboard.jsx
│
├── schemas/
│ │
│ └── studentSchema.js
│
├── services/
│ │
│ └── studentService.js
│
├── hooks/
│ │
│ └── useStudents.js
│
├── App.jsx
└── main.jsx

And each part has a job:

components
↓
Reusable UI
pages
↓
Application screens
schemas
↓
Validation rules
services
↓
API communication
hooks
↓
Reusable React logic
App.jsx
↓
Application routing/layout

The Complete Flow

When the user wants to add a student:

User opens /students/add
↓
React Router loads AddStudent
↓
Lazy Loading downloads AddStudent
↓
Suspense displays loader while waiting
↓
AddStudent renders
↓
React Hook Form manages the form
↓
User enters information
↓
Zod/Yup validates the information
↓
Validation succeeds
↓
onSubmit() runs
↓
studentService sends data to API
↓
Student is created

This is how these concepts connect in a real React application.

What you should remember

Reusable Components
Build once, use everywhere.

Folder Structure
Organize code based on responsibility.

Code Splitting
Break a large JavaScript bundle into smaller pieces.

Lazy Loading
Don’t download code until you need it.

Suspense
Show a fallback while lazy content is loading.

Forms
Collect and manage user information.

Validation
Make sure the information is correct.

React Hook Form
Simplify form state and submission.

Yup/Zod
Create centralized validation schemas.

Project

The next logical step after this lesson is to build the complete Student Manager project, including React Router, a navbar, dashboard, CRUD operations, API integration, reusable form components, and protected routes.

--

--