This content originally appeared on DEV Community and was authored by Biplob Hasan Nibir
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-caseuser-profile.tsx- This prevents issues on case-sensitive systems like Unix.
-
Component Naming β Use
PascalCasefunction 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
defaultunless 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.propNameif 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
buttonTextbefore 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.
This content originally appeared on DEV Community and was authored by Biplob Hasan Nibir