React Best Practices for 2025
Master modern React patterns including hooks, context, performance optimization, and server components with Next.js 14. Your complete guide to building production-ready React applications.
Mubashir Hassan
Full Stack Developer & React Specialist
Table of Contents
Component Architecture & Patterns
Building scalable React applications starts with solid component architecture. The way you structure and organize components determines maintainability, reusability, and team productivity.
1. Container/Presentational Pattern
Separate business logic from presentation. Container components handle data and logic, while presentational components focus purely on UI rendering.
// ❌ Avoid: Mixed concerns
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser);
}, []);
return {user?.name};
}
// ✅ Better: Separated concerns
function UserProfileContainer() {
const user = useUser(); // Custom hook for logic
return ;
}
function UserProfileView({ user }) {
if (!user) return ;
return (
{user.name}
{user.bio}
);
}
2. Composition Over Inheritance
React favors composition. Use children and render props to create flexible, reusable components.
// Compound component pattern
function Card({ children }) {
return {children};
}
Card.Header = ({ children }) => (
{children}
);
Card.Body = ({ children }) => (
{children}
);
// Usage
Title
Content
3. Folder Structure
Organize by feature, not by type. This scales better as your app grows.
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api/
│ │ └── index.ts
│ ├── dashboard/
│ └── settings/
├── shared/
│ ├── components/
│ ├── hooks/
│ └── utils/
└── app/
├── layout.tsx
└── page.tsx
Pro Tip
Use the "Thinking in React" approach: identify component hierarchy, build a static version, identify minimal state, and determine where state should live.
Hooks Mastery
Hooks revolutionized React development. Master these patterns for cleaner, more maintainable code.
useState: Keep It Simple
// ❌ Avoid: Too many useState calls
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
// ✅ Better: Group related state
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
phone: ''
});
// Update specific fields
const updateField = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
useEffect: Understand Dependencies
// ❌ Avoid: Missing dependencies (ESLint will warn)
useEffect(() => {
fetchData(userId);
}, []); // userId should be in deps!
// ✅ Correct: Include all dependencies
useEffect(() => {
fetchData(userId);
}, [userId]);
// ✅ Cleanup subscriptions
useEffect(() => {
const subscription = subscribe(data => {
setData(data);
});
return () => subscription.unsubscribe();
}, []);
// ✅ Async effects
useEffect(() => {
let cancelled = false;
async function fetchData() {
const result = await api.getData();
if (!cancelled) setData(result);
}
fetchData();
return () => { cancelled = true; };
}, []);
Custom Hooks: Reusable Logic
Extract reusable logic into custom hooks. This is one of React's most powerful patterns.
// useLocalStorage hook
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// useDebounce hook
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// Usage
function SearchComponent() {
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 500);
useEffect(() => {
// API call with debounced value
searchAPI(debouncedSearch);
}, [debouncedSearch]);
}
useMemo & useCallback: Optimize Wisely
// ❌ Premature optimization (unnecessary useMemo)
const doubled = useMemo(() => count * 2, [count]);
// ✅ Use for expensive computations
const sortedAndFiltered = useMemo(() => {
return items
.filter(item => item.active)
.sort((a, b) => a.value - b.value);
}, [items]);
// ✅ useCallback for stable function references
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// Pass to memoized child
Common Mistake
Don't overuse useMemo and useCallback. They add overhead. Only use them when you have measured performance issues or when passing callbacks to memoized components.
Modern State Management
State management in 2025 is simpler than ever. Choose the right tool for your needs.
Context + useReducer
Best for: App-wide state (theme, auth, locale)
Simple, built-in solution. No external dependencies. Perfect for most apps.
Zustand
Best for: Complex state without boilerplate
Lightweight (1kb), simple API, great DevTools. Modern Redux alternative.
TanStack Query
Best for: Server state & data fetching
Handles caching, background updates, and error handling automatically.
Jotai / Recoil
Best for: Atomic state management
Bottom-up approach with atoms. Great for complex, derived state.
Context Pattern Example
// Create context with provider
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check auth status
checkAuth().then(setUser).finally(() => setLoading(false));
}, []);
const login = async (credentials) => {
const user = await api.login(credentials);
setUser(user);
};
const logout = () => {
api.logout();
setUser(null);
};
const value = { user, loading, login, logout };
return (
{children}
);
}
// Custom hook for easy consumption
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
TanStack Query Example
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UserProfile({ userId }) {
const queryClient = useQueryClient();
// Fetch user data with auto-caching
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
// Update user mutation
const updateMutation = useMutation({
mutationFn: updateUser,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries(['user', userId]);
},
});
if (isLoading) return ;
return (
{user.name}
);
}
For more on state management, check out React's official docs and TanStack Query documentation.
Performance Optimization
Performance isn't just about speed—it's about user experience. Here's how to build fast React apps.
1. Code Splitting & Lazy Loading
import { lazy, Suspense } from 'react';
// Lazy load heavy components
const Dashboard = lazy(() => import('./Dashboard'));
const Analytics = lazy(() => import('./Analytics'));
function App() {
return (
}>
} />
} />
);
}
// Next.js dynamic imports with options
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./Chart'), {
loading: () => Loading chart...
,
ssr: false, // Disable SSR for this component
});
2. React.memo for Expensive Components
// Prevent unnecessary re-renders
const ExpensiveComponent = React.memo(({ data }) => {
// Complex rendering logic
return {/* Heavy computation */};
});
// Custom comparison function
const UserCard = React.memo(
({ user }) => {user.name},
(prevProps, nextProps) => {
// Only re-render if user.id changes
return prevProps.user.id === nextProps.user.id;
}
);
3. Virtualization for Long Lists
Rendering 10,000 items? Use virtualization to only render visible items.
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
{items[index].name}
);
return (
{Row}
);
}
4. Image Optimization
// Next.js Image component (automatic optimization)
import Image from 'next/image';
// Lazy loading images

Performance Checklist
- Use React DevTools Profiler to identify slow components
- Implement code splitting for routes and heavy components
- Optimize images (WebP, sizing, lazy loading)
- Minimize bundle size (analyze with webpack-bundle-analyzer)
- Use production builds for deployment
Server Components & Next.js 14
Next.js 14 with App Router brings React Server Components to production. This is a game-changer for performance and developer experience.
Server vs Client Components
Server Components (Default)
- Direct database access
- Server-only code (no client bundle)
- Automatic code splitting
- Better SEO
- No state or effects
Client Components ('use client')
- Interactive UI
- useState, useEffect, etc.
- Browser APIs (window, localStorage)
- Event listeners
- Shipped to browser (bundle size)
Server Component Example
// app/dashboard/page.tsx
// Server Component (default in App Router)
import { db } from '@/lib/database';
export default async function DashboardPage() {
// Direct database access!
const stats = await db.query('SELECT * FROM stats');
const users = await db.users.findMany();
return (
Dashboard
);
}
Client Component Example
// components/SearchBar.tsx
'use client'; // Mark as client component
import { useState } from 'react';
export function SearchBar() {
const [query, setQuery] = useState('');
return (
setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
Data Fetching Patterns
// Parallel data fetching
async function Page() {
const [userData, postsData] = await Promise.all([
fetch('/api/user'),
fetch('/api/posts')
]);
const user = await userData.json();
const posts = await postsData.json();
return ;
}
// Streaming with Suspense
import { Suspense } from 'react';
function Page() {
return (
Dashboard
}>
);
}
Why Server Components?
Server Components reduce client-side JavaScript by an average of 70%, resulting in faster page loads and better performance on low-end devices.
Learn more in the Next.js documentation.
TypeScript Best Practices
TypeScript is now the standard for React development. Here's how to use it effectively.
Component Props Types
// Define prop types
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg'; // Optional
onClick: () => void;
children: React.ReactNode;
disabled?: boolean;
}
// Use in component
function Button({
variant,
size = 'md', // Default value
onClick,
children,
disabled = false
}: ButtonProps) {
return (
);
}
// Generic component types
interface ListProps {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List({ items, renderItem }: ListProps) {
return (
{items.map((item, i) => (
- {renderItem(item)}
))}
);
}
Hooks with TypeScript
// useState with type inference
const [count, setCount] = useState(0); // number inferred
// Explicit type for complex state
const [user, setUser] = useState(null);
// useRef types
const inputRef = useRef(null);
const divRef = useRef(null);
// Custom hook with return type
function useUser(id: string): {
user: User | null;
loading: boolean;
error: Error | null;
} {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser(id)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [id]);
return { user, loading, error };
}
For comprehensive TypeScript + React guides, check out the React TypeScript Cheatsheet.
Testing Strategies
Testing ensures your React app works as expected. Focus on testing behavior, not implementation.
Testing Library Best Practices
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('user can submit form', async () => {
const handleSubmit = jest.fn();
render( );
// Find elements by accessible roles/labels
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /login/i });
// Simulate user interaction
await userEvent.type(emailInput, 'user@example.com');
await userEvent.type(passwordInput, 'password123');
await userEvent.click(submitButton);
// Assert behavior
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123'
});
});
});
Testing Custom Hooks
import { renderHook, act } from '@testing-library/react';
test('useCounter increments', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Testing Pyramid for React
- 70% Unit Tests: Test individual components and hooks in isolation
- 20% Integration Tests: Test component interactions and data flow
- 10% E2E Tests: Test critical user journeys with Playwright/Cypress
For more testing guidance, see React Testing Library docs.
Conclusion: Building Production-Ready React Apps
React in 2025 is more powerful and developer-friendly than ever. Server Components, improved hooks, and mature tooling make it easier to build fast, scalable applications.
Remember these key principles:
- Component composition over complex hierarchies
- Custom hooks for reusable logic
- Server Components for better performance
- TypeScript for type safety
- Test behavior, not implementation
Need help building your next React project? Check out my development services or get in touch for a consultation.
Ready to Build Better React Apps?
Let's discuss your next React project and how to implement these best practices.
Start Your React Project