ReactJs Best Practices and Patterns
Writing React code that is maintainable, performant, and scalable requires following established patterns and best practices. This guide covers proven approaches that professional React developers use to build production-ready applications.
Project Structure
Organize your project for scalability and maintainability. While React doesn’t enforce a specific structure, these patterns have proven effective:
Feature-Based Organization
Group files by feature rather than by type:
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm.jsx
│ │ │ └── SignupForm.jsx
│ │ ├── hooks/
│ │ │ └── useAuth.js
│ │ ├── services/
│ │ │ └── authService.js
│ │ └── index.js
│ ├── products/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── services/
│ └── cart/
├── shared/
│ ├── components/
│ ├── hooks/
│ └── utils/
├── App.jsx
└── main.jsx
This structure keeps related code together, making it easier to find and modify features.
Component Organization
Each component folder can include:
Button/
├── Button.jsx # Component implementation
├── Button.test.jsx # Tests
├── Button.module.css # Styles
├── Button.stories.jsx # Storybook stories (if using)
└── index.js # Re-export for clean imports
Component Patterns
Keep Components Small and Focused
Each component should have a single responsibility:
// Bad - component doing too much
function UserDashboard({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [analytics, setAnalytics] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
fetchPosts(userId).then(setPosts);
fetchAnalytics(userId).then(setAnalytics);
}, [userId]);
return (
<div>
{/* Lots of JSX mixing user info, posts, and analytics */}
</div>
);
}
// Good - split into focused components
function UserDashboard({ userId }) {
return (
<div>
<UserProfile userId={userId} />
<UserPosts userId={userId} />
<UserAnalytics userId={userId} />
</div>
);
}
function UserProfile({ userId }) {
const { data: user, loading, error } = useUser(userId);
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <ProfileCard user={user} />;
}
Prefer Composition Over Props
Use the children prop for flexible composition:
// Less flexible
function Modal({ title, content, footer }) {
return (
<div className="modal">
<h2>{title}</h2>
<div>{content}</div>
<div>{footer}</div>
</div>
);
}
// More flexible
function Modal({ children }) {
return <div className="modal">{children}</div>;
}
function Modal.Header({ children }) {
return <div className="modal-header">{children}</div>;
}
function Modal.Body({ children }) {
return <div className="modal-body">{children}</div>;
}
function Modal.Footer({ children }) {
return <div className="modal-footer">{children}</div>;
}
// Usage
<Modal>
<Modal.Header>
<h2>Confirm Action</h2>
</Modal.Header>
<Modal.Body>
<p>Are you sure you want to proceed?</p>
</Modal.Body>
<Modal.Footer>
<Button onClick={handleConfirm}>Confirm</Button>
<Button onClick={handleCancel}>Cancel</Button>
</Modal.Footer>
</Modal>
Extract Custom Hooks
Move reusable logic into custom Hooks:
// Custom hook for data fetching
function useData(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(response => response.json())
.then(data => {
if (!cancelled) {
setData(data);
setLoading(false);
}
})
.catch(error => {
if (!cancelled) {
setError(error);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data: users, loading, error } = useData('/api/users');
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
State Management Patterns
Colocate State
Keep state as close as possible to where it’s used:
// Bad - state too high in tree
function App() {
const [modalOpen, setModalOpen] = useState(false);
return (
<div>
<Header />
<MainContent />
<Footer>
<SettingsButton onClick={() => setModalOpen(true)} />
</Footer>
{modalOpen && <SettingsModal onClose={() => setModalOpen(false)} />}
</div>
);
}
// Good - state colocated with usage
function App() {
return (
<div>
<Header />
<MainContent />
<Footer>
<Settings />
</Footer>
</div>
);
}
function Settings() {
const [modalOpen, setModalOpen] = useState(false);
return (
<>
<SettingsButton onClick={() => setModalOpen(true)} />
{modalOpen && <SettingsModal onClose={() => setModalOpen(false)} />}
</>
);
}
Use Reducers for Complex State
When state logic becomes complex, use useReducer:
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, data: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
case 'UPDATE_ITEM':
return {
...state,
data: state.data.map(item =>
item.id === action.payload.id ? action.payload : item
)
};
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function DataManager() {
const [state, dispatch] = useReducer(reducer, {
data: [],
loading: false,
error: null
});
useEffect(() => {
dispatch({ type: 'FETCH_START' });
fetchData()
.then(data => dispatch({ type: 'FETCH_SUCCESS', payload: data }))
.catch(error => dispatch({ type: 'FETCH_ERROR', payload: error }));
}, []);
return (
<div>
{state.loading && <Spinner />}
{state.error && <Error message={state.error.message} />}
{state.data.map(item => (
<Item
key={item.id}
item={item}
onUpdate={(updated) =>
dispatch({ type: 'UPDATE_ITEM', payload: updated })
}
/>
))}
</div>
);
}
Performance Optimization
Don’t Optimize Prematurely
Measure before optimizing. Use React DevTools Profiler to identify actual performance issues.
Memoize Expensive Computations
Use useMemo for expensive calculations:
function ProductList({ products, filters }) {
const filteredProducts = useMemo(() => {
return products.filter(product => {
return Object.entries(filters).every(([key, value]) => {
return product[key] === value;
});
});
}, [products, filters]);
return (
<ul>
{filteredProducts.map(product => (
<ProductItem key={product.id} product={product} />
))}
</ul>
);
}
Memoize Callbacks
Use useCallback when passing callbacks to memoized components:
function TodoList({ todos }) {
const [filter, setFilter] = useState('all');
const handleToggle = useCallback((id) => {
toggleTodo(id);
}, []);
const handleDelete = useCallback((id) => {
deleteTodo(id);
}, []);
return (
<div>
<FilterBar value={filter} onChange={setFilter} />
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</div>
);
}
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
);
});
Use Code Splitting
Split large components into separate bundles:
import { lazy, Suspense } from 'react';
const AdminPanel = lazy(() => import('./AdminPanel'));
const UserDashboard = lazy(() => import('./UserDashboard'));
function App() {
const { user } = useAuth();
return (
<Suspense fallback={<LoadingSpinner />}>
{user.isAdmin ? <AdminPanel /> : <UserDashboard />}
</Suspense>
);
}
Error Handling
Error Boundaries
Create error boundaries to catch and handle errors gracefully:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
// Log to error reporting service
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.toString()}</pre>
</details>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<MainApp />
</ErrorBoundary>
);
}
Handle Async Errors
Always handle errors in async operations:
function DataComponent() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const json = await response.json();
if (!cancelled) {
setData(json);
}
} catch (err) {
if (!cancelled) {
setError(err);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, []);
if (loading) return <Spinner />;
if (error) return <ErrorDisplay error={error} />;
return <DataDisplay data={data} />;
}
Testing Best Practices
Test User Behavior, Not Implementation
Focus on what users do, not how the code works:
import { render, screen, userEvent } from '@testing-library/react';
test('allows user to add a todo', async () => {
render(<TodoApp />);
const input = screen.getByPlaceholderText(/add a todo/i);
const button = screen.getByRole('button', { name: /add/i });
await userEvent.type(input, 'Buy milk');
await userEvent.click(button);
expect(screen.getByText('Buy milk')).toBeInTheDocument();
});
Use Testing Library Queries
Prefer queries that match how users interact:
// Best - accessible to all users
screen.getByRole('button', { name: /submit/i })
// Good - visible to users
screen.getByLabelText(/username/i)
screen.getByText(/welcome/i)
// Okay - fallback for non-semantic content
screen.getByTestId('submit-button')
// Avoid - implementation details
container.querySelector('.submit-button')
Accessibility
Use Semantic HTML
Prefer semantic elements over divs:
// Bad
<div onClick={handleClick}>Click me</div>
// Good
<button onClick={handleClick}>Click me</button>
// Bad
<div className="heading">Title</div>
// Good
<h2>Title</h2>
Provide ARIA Labels
Add labels for screen readers:
function SearchForm() {
return (
<form role="search">
<input
type="search"
aria-label="Search products"
placeholder="Search..."
/>
<button type="submit" aria-label="Submit search">
<SearchIcon />
</button>
</form>
);
}
Manage Focus
Handle focus for keyboard navigation:
function Modal({ isOpen, onClose, children }) {
const closeButtonRef = useRef(null);
useEffect(() => {
if (isOpen) {
closeButtonRef.current?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div role="dialog" aria-modal="true">
<button
ref={closeButtonRef}
onClick={onClose}
aria-label="Close modal"
>
×
</button>
{children}
</div>
);
}
TypeScript with React
TypeScript adds type safety to React applications:
interface User {
id: number;
name: string;
email: string;
}
interface UserCardProps {
user: User;
onEdit: (user: User) => void;
onDelete: (id: number) => void;
}
function UserCard({ user, onEdit, onDelete }: UserCardProps) {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => onEdit(user)}>Edit</button>
<button onClick={() => onDelete(user.id)}>Delete</button>
</div>
);
}