React best practices are recommended approaches for writing React applications that are readable, reusable, maintainable, predictable, and easier to debug. Good practices become especially important as an application grows and contains many components, pages, API calls, and developers.
Create components that can be reused in different parts of an application.
function Button({ title }) {
return (
<button className="btn btn-primary">
{title}
</button>
);
}
The same component can be used for different buttons.
A component should generally have a clear responsibility.
Dashboard
├── Header
├── Sidebar
├── Statistics
├── UserList
└── Footer
Breaking a large interface into meaningful components can make the code easier to understand and maintain.
Component names should describe what the component represents.
function StudentCard() {
return <div>Student</div>;
}
function CourseList() {
return <div>Courses</div>;
}
Names such as StudentCard and CourseList are easier
to understand than unclear names such as Box1 or
Component2.
Very large components can become difficult to read and maintain.
Instead of putting an entire application page into one component, separate meaningful sections into components.
function Dashboard() {
return (
<>
<Header />
<Statistics />
<RecentOrders />
<Footer />
</>
);
}
State should generally live in the component or the nearest appropriate common parent that needs it.
function SearchBox() {
const [search, setSearch] =
useState("");
return (
<input
value={search}
onChange={e =>
setSearch(
e.target.value
)
}
/>
);
}
Do not move every piece of state into a global store if only one component needs it.
Avoid storing information that can easily be calculated from existing state.
const fullName =
firstName + " " + lastName;
There is usually no need to create separate state for fullName
when it can be derived from firstName and
lastName.
Props provide a clear way to pass data from a parent component to a child component.
function StudentCard({
name,
course
}) {
return (
<div>
<h3>
{name}
</h3>
<p>
{course}
</p>
</div>
);
}
Destructuring can make component parameters easier to read.
function CourseCard({
title,
fee,
duration
}) {
return (
<div>
<h2>
{title}
</h2>
<p>
Fee: {fee}
</p>
<p>
Duration: {duration}
</p>
</div>
);
}
When rendering a list, provide a stable key for each item.
{students.map(student => (
<StudentCard
key={student.id}
student={student}
/>
))}
A stable identifier is generally preferable to generating a new random key during every render.
Readable JSX is easier to understand and debug.
function Profile() {
return (
<div className="profile">
<h1>
Student Profile
</h1>
<p>
Welcome to the dashboard.
</p>
</div>
);
}
Use indentation and meaningful structure rather than putting a large amount of JSX on one line.
Use useEffect for side effects and synchronization with external
systems. Do not use it simply to calculate values that can be calculated
during rendering.
const total =
price * quantity;
A simple derived value usually does not need an effect.
An effect should have a clear purpose.
useEffect(() => {
document.title =
"React Course";
}, []);
Avoid placing unrelated operations into one large effect when separate effects would make the logic clearer.
Applications that load data should clearly represent the loading state.
if (loading) {
return (
<p>
Loading...
</p>
);
}
This provides feedback while asynchronous work is in progress.
API requests and other asynchronous operations can fail. Handle errors instead of assuming every request succeeds.
if (error) {
return (
<div className="alert alert-danger">
{error}
</div>
);
}
For larger applications, keeping API-related code organized can make components easier to maintain.
src/
├── components/
├── pages/
├── services/
│ └── api.js
├── hooks/
└── App.jsx
A service layer or API helper can centralize repeated request logic.
Custom Hooks can extract reusable stateful logic.
function useCounter() {
const [count, setCount] =
useState(0);
function increment() {
setCount(
value => value + 1
);
}
return {
count,
increment
};
}
Context is useful for values that need to be shared across many components.
Examples include:
Do not automatically put every piece of application state into Context.
Give meaningful names to event handler functions.
function handleSubmit(e) {
e.preventDefault();
// Submit form
}
return (
<form
onSubmit={handleSubmit}
>
...
</form>
);
Validate user input before submitting forms and provide useful feedback.
if (!email) {
setError(
"Email is required"
);
return;
}
Important validation should also be performed by the backend because frontend validation alone is not a security boundary.
Authentication state and operations can be organized in a dedicated authentication context or custom hook.
const {
user,
login,
logout
} = useAuth();
This can prevent authentication logic from being duplicated across many components.
Frontend route protection is not enough to secure sensitive data.
React UI
↓
Protected Route
↓
API Request
↓
Backend Authentication
↓
Backend Authorization
↓
Protected Data
The server should verify that the user is allowed to perform the requested operation.
Do not add useMemo, useCallback, or
React.memo everywhere without a reason.
Measure
↓
Find Problem
↓
Optimize
↓
Measure Again
Performance optimization should solve a measured or clearly understood problem.
Large applications can split code so that features are loaded when needed.
const Admin =
lazy(() =>
import("./Admin")
);
This can reduce the amount of JavaScript needed for the initial application load.
Large lists can require substantial rendering work.
Possible approaches include:
Users
↓
Filter
↓
Paginate
↓
Render Required Items
A consistent project structure helps developers locate code quickly.
src/
├── components/
├── pages/
├── hooks/
├── services/
├── context/
├── assets/
├── App.jsx
└── main.jsx
The exact structure can vary depending on the size and requirements of the application.
Environment variables can be useful for configuration such as API base URLs.
const API_URL =
import.meta.env.VITE_API_URL;
Error boundaries can provide fallback UI when a descendant component throws a rendering error.
Application
↓
Error Boundary
↓
React Components
↓
Error
↓
Fallback UI
Error boundaries are useful for preventing one rendering failure from producing an uncontrolled user experience across a larger interface.
import {
useEffect,
useState
} from "react";
function StudentList() {
const [students, setStudents] =
useState([]);
const [loading, setLoading] =
useState(true);
const [error, setError] =
useState("");
useEffect(() => {
async function loadStudents() {
try {
const response =
await fetch(
"/api/students"
);
if (!response.ok) {
throw new Error(
"Failed to load students"
);
}
const data =
await response.json();
setStudents(data);
} catch (err) {
setError(
err.message
);
} finally {
setLoading(false);
}
}
loadStudents();
}, []);
if (loading) {
return (
<p>
Loading students...
</p>
);
}
if (error) {
return (
<div className="alert alert-danger">
{error}
</div>
);
}
return (
<div>
<h2>
Students
</h2>
{students.map(
student => (
<div
key={student.id}
className="card mb-2"
>
<div className="card-body">
<h5>
{student.name}
</h5>
</div>
</div>
))}
</div>
);
}
export default StudentList;
Clean Components
↓
Clear Data Flow
↓
Organized Logic
↓
Reliable Application
↓
Maintainable React Project
Question: Which is a good React best practice?