Clean code isn't just about making things work — it's about making them easy to read, maintain, and scale.
React gives us incredible flexibility, but without conventions, a codebase can quickly turn messy. In this guide, we'll walk through industry best practices for structuring React components: from naming files to organizing logic, handling props, and writing beautiful JSX.
By the end, you'll have a blueprint for writing professional-grade React components that stand the test of time, teamwork, and scaling.
🔹 1. Naming and Structure
Consistency is the backbone of a clean codebase.
-
File Naming → Use
kebab-case
user-profile.tsx
- This prevents issues on case-sensitive systems like Unix.
-
Component Naming → Use
PascalCase
function UserProfile() { ... }
-
Function Declaration vs. Arrow Function →
- Prefer function declarations for components. They help dev tools and linters infer the component name automatically, making debugging easier.
-
Export → Always export as
default
unless there's a strong reason not to.
🔹 2. Props and Logic
Props define a component's contract. Handle them carefully.
-
Use Interfaces Instead of Types
- Interfaces are extensible and provide clearer TypeScript errors.
-
Destructuring vs.
props.propName
- ✅ Use destructuring if you have 1–3 props.
- ✅ Use
props.propName
if you have many props (keeps the function signature clean).
-
Logical Order Inside a Component
- Keep your component predictable by following this order:
- Custom hooks
-
useState
-
useRef
- Helper functions
- Event handlers
-
useEffect
- Early returns
- JSX return
- Keep your component predictable by following this order:
-
Helper Functions
- If used only in one component → keep it inside.
- If reusable → move it into a
utils/
file.
-
Early Returns
- Handle loading or error states first to avoid deeply nested JSX.
🔹 3. Clean Code and Readability
Readable code = maintainable code.
-
Render Variables
- Define variables like
buttonText
before your JSX. Don't clutter JSX with inline logic.
- Define variables like
-
Semantic HTML
- Use meaningful tags (
<section>
,<header>
,<footer>
) instead of endless<div>
s.
- Use meaningful tags (
-
Styling Consistency
- Choose one styling system (e.g., Tailwind, CSS Modules) and stick to it. Mixing multiple systems makes maintenance harder.
-
Refactor When Needed
- If a component grows too big → break it into smaller components or custom hooks.
🔹 4. Example: The Perfect UserProfile
Component
Here's a component that puts all these principles into practice:
// src/components/user-profile.tsx
import React, { useState, useEffect, useRef } from 'react';
import { useAuth } from '../hooks/useAuth';
import { fetchUserData } from '../utils/api';
interface UserProfileProps {
userId: string;
isEditor?: boolean;
}
export default function UserProfile({ userId, isEditor }: UserProfileProps) {
/**
* 1. Custom Hooks
*/
const { isAuthorized } = useAuth();
/**
* 2. useState
*/
const [user, setUser] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
/**
* 3. useRef
* Example: track number of profile loads
*/
const loadCountRef = useRef<number>(0);
/**
* 4. Helper Functions
*/
const formatLocation = (location: string) => {
return location ? location.toUpperCase() : 'Unknown';
};
/**
* 5. Event Handlers
*/
const handleEditClick = () => {
alert('Editing profile...');
};
/**
* 6. useEffect
*/
useEffect(() => {
const getData = async () => {
const userData = await fetchUserData(userId);
setUser(userData);
setIsLoading(false);
// increment load count
loadCountRef.current += 1;
};
getData();
}, [userId]);
/**
* 7. Early Returns
*/
if (isLoading) return <div>Loading user profile...</div>;
if (!user) return <div>User not found.</div>;
/**
* Render Logic Variables
*/
const buttonText = isAuthorized ? 'Edit Profile' : 'View Profile';
/**
* 8. JSX Return
*/
return (
<section className="user-profile border rounded p-4 shadow-sm">
<header className="flex items-center justify-between mb-4">
<h1 className="text-xl font-bold">{user.name}</h1>
{isEditor && (
<button
onClick={handleEditClick}
className="bg-blue-500 text-white px-3 py-1 rounded"
>
{buttonText}
</button>
)}
</header>
<div className="profile-details space-y-2">
<p>Email: {user.email}</p>
<p>Location: {formatLocation(user.location)}</p>
<p className="text-sm text-gray-500">
Profile loaded {loadCountRef.current} times
</p>
</div>
</section>
);
}
🔹 5. Modern React with Concurrent Features
React 18+ introduced concurrent rendering features that make UIs feel smoother and more interactive.
- Suspense → declarative way to handle async loading
- useTransition → keeps UI responsive during state updates
- useDeferredValue → prevents lag when typing in search inputs
These hooks are game changers for building modern, responsive apps.
🔹 6. Example: SearchUsers
Component (Modern React)
// src/components/search-users.tsx
"use client";
import React, {
useState,
useTransition,
Suspense,
useDeferredValue,
useEffect,
} from "react";
import { fetchUsers } from "../utils/api";
interface User {
id: string;
name: string;
email: string;
}
/**
* Child Component: Renders a list of users
*/
function UserList({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
const [users, setUsers] = useState<User[]>([]);
const [isPending, startTransition] = useTransition();
useEffect(() => {
startTransition(async () => {
const data = await fetchUsers(deferredQuery);
setUsers(data);
});
}, [deferredQuery]);
if (isPending) return <p>Loading users...</p>;
if (!users.length) return <p>No users found.</p>;
return (
<ul className="space-y-2">
{users.map((u) => (
<li key={u.id} className="p-2 border rounded">
<strong>{u.name}</strong> - {u.email}
</li>
))}
</ul>
);
}
/**
* Parent Component: Search box + Suspense
*/
export default function SearchUsers() {
const [query, setQuery] = useState("");
return (
<section className="search-users border rounded p-4 shadow-sm">
<header className="mb-4">
<h2 className="text-xl font-bold">Search Users</h2>
<input
type="text"
placeholder="Type a name..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="p-2 border rounded w-full"
/>
</header>
<Suspense fallback={<p>Loading search results...</p>}>
<UserList query={query} />
</Suspense>
</section>
);
}
🔹 7. Why This Matters
- Suspense → keeps loading logic clean & declarative
- useTransition → prevents input lag while fetching data
- useDeferredValue → delays updates to avoid expensive re-renders on each keystroke
💡 Pro tip: Try adding useOptimistic
(React 19) for instant UI feedback when mutating data. It makes user actions feel real-time.
Together, these features make apps feel fast, smooth, and modern.
✅ Final Key Takeaways
- Consistency matters most. Use the same conventions everywhere.
- Readable code saves time. Early returns, semantic HTML, and render variables keep things clear.
- Refactor for modularity. Break down complex components into smaller, reusable ones.
- Adopt modern React. Use Suspense, transitions, and deferred values for the best user experience.
💡 Pro tip: Take one of your old, messy components and refactor it with these rules. You'll instantly see the difference in clarity, maintainability, and performance.
Top comments (0)