Development 12 min read

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.

MH

Mubashir Hassan

Full Stack Developer & React Specialist

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';

Description

// Lazy loading images
Description

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