React Best Practices to Up Your Game in 2023

Table of Contents

Why React Best Practices Matter

When you install React in Visual Studio Code and start building applications, the code you write today will be maintained for months or years. Best practices aren’t optional—they’re the difference between:

  • ✅ A maintainable, scalable application
  • ❌ A technical debt nightmare

The Cost of Ignoring Best Practices

Poor Practice Impact:

Initial Project: 100 lines of code (1 day to write)
6 Months Later: Bugs everywhere (40+ hours to debug)
Scaling: Impossible (rewrites required)
Team: Frustrated (code is unreadable)

Best Practice Impact:

Initial Project: 120 lines of code (1.2 days to write)
6 Months Later: Few bugs (2-3 hours to maintain)
Scaling: Smooth (modular, tested code)
Team: Happy (code is self-documenting)

ROI of Best Practices: 15-20x time savings over project lifetime.

Mastering Functional Components & Hooks (Modern Standard)

Historical Context: React class components are deprecated in modern React development. Functional components with Hooks are now the standard.

Understanding Functional Components

A functional component is simply a JavaScript function that returns JSX:

javascript

// ✅ MODERN: Functional Component (Recommended)
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default Counter;

Essential Hooks Every Developer Needs

useState: Manage component state

javascript

const [isLoading, setIsLoading] = useState(false);
const [user, setUser] = useState(null);
const [errors, setErrors] = useState([]);

useEffect: Handle side effects (API calls, timers, etc.)

javascript

useEffect(() => {
  // Runs after component renders
  fetchUserData();
  
  // Cleanup function (optional)
  return () => {
    // Cleanup on unmount
  };
}, [userId]); // Dependency array

useContext: Access global data without prop drilling

javascript

const theme = useContext(ThemeContext);
const user = useContext(UserContext);

useReducer: Complex state logic

javascript

const [state, dispatch] = useReducer(reducer, initialState);

function reducer(state, action) {
  switch(action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

Custom Hooks: Reuse Logic Across Components

Extract reusable logic into custom hooks:

javascript

// Custom Hook: useFetch
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [url]);

  return { data, loading, error };
}

// Usage in multiple components
function UserProfile() {
  const { data: user, loading } = useFetch('/api/user');
  // ...
}

Benefits of Custom Hooks:

  • ✅ Reduce code duplication
  • ✅ Easier to test
  • ✅ Share logic between components
  • ✅ Cleaner component code

Optimizing Component Rendering for Performance

Performance optimization is critical for user experience. Slow React apps lose users.

Understanding Re-renders

React components re-render when:

  • Parent component re-renders
  • Props change
  • State changes
  • Context changes

Not all re-renders are necessary.

Preventing Unnecessary Re-renders

Method 1: React.memo (Functional Components)

javascript

// Without memo: Child re-renders even if props don't change
function ParentComponent() {
  const [count, setCount] = useState(0);
  
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveChild name="John" /> {/* Re-renders on every parent render */}
    </>
  );
}

// With memo: Child only re-renders if props change
const ExpensiveChild = React.memo(({ name }) => {
  console.log('Child rendered');
  return <h1>Hello, {name}</h1>;
});

Method 2: useMemo Hook

Cache expensive computations:

javascript

function SearchResults({ query, data }) {
  // Without useMemo: filteredResults recalculated on every render
  // With useMemo: only recalculated when query or data changes
  const filteredResults = useMemo(() => {
    return data.filter(item => 
      item.name.toLowerCase().includes(query.toLowerCase())
    );
  }, [query, data]);

  return <div>{filteredResults.length} results found</div>;
}

Method 3: useCallback Hook

Memoize functions to avoid re-creating on every render:

javascript

function Parent() {
  const [count, setCount] = useState(0);
  
  // Without useCallback: handleClick recreated on every render
  // With useCallback: same function reference if dependencies don't change
  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return <Child onClick={handleClick} />;
}

Measuring Performance

Use React DevTools Profiler:

javascript

// Wrap component to measure render time
import { Profiler } from 'react';

function App() {
  return (
    <Profiler id="AppRoot" onRender={onRenderCallback}>
      <MainComponent />
    </Profiler>
  );
}

function onRenderCallback(id, phase, actualDuration) {
  console.log(`${id} (${phase}) took ${actualDuration}ms`);
}

Implementing Proper State Management

State management becomes critical as apps grow. Choose the right tool for your needs.

Local State (useState)

For component-level state:

javascript

function TodoItem({ todo }) {
  const [isEditing, setIsEditing] = useState(false);
  
  return (
    <div>
      {isEditing ? (
        <input defaultValue={todo.text} />
      ) : (
        <p>{todo.text}</p>
      )}
      <button onClick={() => setIsEditing(!isEditing)}>
        {isEditing ? 'Save' : 'Edit'}
      </button>
    </div>
  );
}

When to use: Component-specific UI state (modals, forms, toggles)

Context API (useContext)

For shared state without prop drilling:

javascript

// Create context
const UserContext = createContext();

// Provider
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

// Consumer
function UserProfile() {
  const { user } = useContext(UserContext);
  return <h1>Welcome, {user?.name}</h1>;
}

When to use: App-wide data (theme, user, language)

Redux (Complex State)

When data filtering or complex state interactions become necessary:

javascript

// Action
const INCREMENT = 'INCREMENT';

// Reducer
function counterReducer(state = 0, action) {
  switch(action.type) {
    case INCREMENT:
      return state + 1;
    default:
      return state;
  }
}

// Store (using Redux Toolkit - modern approach)
import { createSlice, configureStore } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: (state) => state + 1,
    decrement: (state) => state - 1,
  }
});

const store = configureStore({
  reducer: counterSlice.reducer
});

When to use: Complex applications with lots of shared state

Choosing Your State Management Solution

SolutionUse CaseComplexity
useStateLocal component stateLow
useReducerComplex local stateLow-Medium
Context APIGlobal UI state (theme, auth)Medium
Redux/ZustandLarge app with complex stateHigh

Best Practice: Start simple (useState), add complexity only as needed.

Writing Tests That Matter

Code without tests is broken code you haven’t found yet.

Unit Tests with Jest

Test individual functions and components:

javascript

// Component to test
function Add({ a, b }) {
  return <div>{a + b}</div>;
}

// Jest test
import { render, screen } from '@testing-library/react';

describe('Add Component', () => {
  it('renders sum of two numbers', () => {
    render(<Add a={2} b={3} />);
    expect(screen.getByText('5')).toBeInTheDocument();
  });

  it('handles negative numbers', () => {
    render(<Add a={-5} b={3} />);
    expect(screen.getByText('-2')).toBeInTheDocument();
  });
});

Integration Tests with React Testing Library

Test how components work together:

javascript

describe('LoginForm Integration', () => {
  it('allows user to log in', async () => {
    render(<LoginForm onLogin={mockOnLogin} />);
    
    // User enters credentials
    fireEvent.change(screen.getByLabelText('Email'), {
      target: { value: 'test@example.com' }
    });
    fireEvent.change(screen.getByLabelText('Password'), {
      target: { value: 'password123' }
    });
    
    // User clicks login
    fireEvent.click(screen.getByRole('button', { name: /login/i }));
    
    // Assert login was called
    await waitFor(() => {
      expect(mockOnLogin).toHaveBeenCalledWith({
        email: 'test@example.com',
        password: 'password123'
      });
    });
  });
});

Testing Best Practices

javascript

// ❌ DON'T: Test implementation details
test('state updates correctly', () => {
  const { getByRole } = render(<Counter />);
  const state = component.state('count'); // Bad!
});

// ✅ DO: Test user behavior
test('increment button increases count', () => {
  render(<Counter />);
  fireEvent.click(screen.getByRole('button', { name: /increment/i }));
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

Coverage Targets:

  • ✅ Critical paths: 100% coverage
  • ✅ Standard logic: 80%+ coverage
  • ✅ UI feedback: Not all needed
  • ❌ Don’t aim for 100% coverage blindly

Leveraging TypeScript for Type Safety

TypeScript catches bugs before they reach users.

Basic TypeScript Setup

javascript

// ❌ JavaScript: Runtime error
function getUser(userId) {
  return fetch(`/api/users/${userId}`)
    .then(res => res.json());
}

const user = getUser('abc'); // Returns undefined, no error

// ✅ TypeScript: Caught at compile time
interface User {
  id: number;
  name: string;
  email: string;
}

function getUser(userId: number): Promise<User> {
  return fetch(`/api/users/${userId}`)
    .then(res => res.json());
}

const user = getUser('abc'); // ❌ TypeScript Error: Argument of type 'string' is not assignable to parameter of type 'number'

Typing React Components

typescript

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

const Button: React.FC<ButtonProps> = ({ label, onClick, disabled }) => (
  <button onClick={onClick} disabled={disabled}>
    {label}
  </button>
);

Typing Hooks

typescript

interface User {
  id: number;
  name: string;
}

function useUser(userId: number): [User | null, boolean] {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUser(userId).then(data => {
      setUser(data);
      setLoading(false);
    });
  }, [userId]);

  return [user, loading];
}

TypeScript Benefits:

  • ✅ Catch bugs at compile time
  • ✅ Better IDE autocomplete
  • ✅ Self-documenting code
  • ✅ Refactoring confidence

Building Accessible React Applications

Accessibility isn’t optional—it’s a legal and moral obligation.

Semantic HTML

javascript

// ❌ NOT accessible
<div onClick={() => navigate('/page')}>Click me</div>

// ✅ Accessible
<a href="/page">Click me</a>

// ❌ NOT accessible
<div className="heading">Main Title</div>

// ✅ Accessible
<h1>Main Title</h1>

ARIA Attributes

javascript

// For interactive elements
<button aria-label="Close menu" onClick={closeMenu}>
  ×
</button>

// For live regions
<div aria-live="polite" aria-atomic="true">
  {notificationMessage}
</div>

// For images
<img src="chart.png" alt="Q3 sales increasing 15%" />

Keyboard Navigation

javascript

function Modal({ isOpen, onClose }) {
  useEffect(() => {
    const handleKeyDown = (e) => {
      if (e.key === 'Escape' && isOpen) {
        onClose();
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [isOpen, onClose]);

  return (
    <div role="dialog" aria-modal="true">
      <button onClick={onClose}>Close</button>
    </div>
  );
}

Testing Accessibility

javascript

import { axe, toHaveNoViolations } from 'jest-axe';

test('has no accessibility violations', async () => {
  const { container } = render(<App />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Handling Errors Gracefully

Errors will happen. How you handle them defines your app’s quality.

Error Boundaries

javascript

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Error caught:', error, errorInfo);
    // Send to error tracking service
    logErrorToService(error);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong. Please refresh the page.</h1>;
    }

    return this.props.children;
  }
}

// Usage
<ErrorBoundary>
  <App />
</ErrorBoundary>

Handling Promise Rejections

javascript

function useAsync(asyncFunction, immediate = true) {
  const [status, setStatus] = useState('idle');
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  const execute = useCallback(async () => {
    setStatus('pending');
    try {
      const response = await asyncFunction();
      setData(response);
      setStatus('success');
    } catch (err) {
      setError(err);
      setStatus('error');
    }
  }, [asyncFunction]);

  useEffect(() => {
    if (immediate) {
      execute();
    }
  }, [execute, immediate]);

  return { execute, status, data, error };
}

Styling Best Practices

Maintainable styling prevents CSS chaos.

CSS Modules (Scoped Styling)

css

/* Button.module.css */
.button {
  background-color: #007bff;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.button:hover {
  background-color: #0056b3;
}

.button.primary {
  background-color: #28a745;
}

javascript

// Button.jsx
import styles from './Button.module.css';

function Button({ variant = 'default', children }) {
  return (
    <button className={`${styles.button} ${styles[variant]}`}>
      {children}
    </button>
  );
}

CSS-in-JS with Styled Components

javascript

import styled from 'styled-components';

const StyledButton = styled.button`
  background-color: #007bff;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background-color: #0056b3;
  }

  ${props => props.primary && `
    background-color: #28a745;
  `}
`;

function Button({ primary, children }) {
  return <StyledButton primary={primary}>{children}</StyledButton>;
}

Code Organization & Modularization

Well-organized code is maintainable code.

Folder Structure

src/
├── components/
│   ├── Button/
│   │   ├── Button.jsx
│   │   ├── Button.test.jsx
│   │   └── Button.module.css
│   ├── Form/
│   │   ├── Form.jsx
│   │   ├── Form.test.jsx
│   │   └── useForm.js
│   └── Modal/
├── hooks/
│   ├── useFetch.js
│   ├── useAsync.js
│   └── useLocalStorage.js
├── utils/
│   ├── api.js
│   ├── helpers.js
│   └── validators.js
├── pages/
│   ├── Home.jsx
│   ├── About.jsx
│   └── NotFound.jsx
├── App.jsx
└── index.jsx

Naming Conventions

javascript

// ✅ Good naming
export const useUserData = () => { /* ... */ };
export const UserCard = ({ user }) => { /* ... */ };
export const fetchUserById = (id) => { /* ... */ };
export const isValidEmail = (email) => { /* ... */ };

// ❌ Poor naming
export const getData = () => { /* ... */ };
export const Card = ({ data }) => { /* ... */ };
export const getInfo = (id) => { /* ... */ };
export const check = (email) => { /* ... */ };

Performance Optimization Strategies

After fixing React setup errors, performance becomes the next priority.

Code Splitting

javascript

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Image Optimization

javascript

// ✅ Optimize images
import { Image } from 'next/image'; // Next.js auto-optimizes

<Image
  src="/avatar.jpg"
  alt="User avatar"
  width={50}
  height={50}
/>

// ✅ Responsive images
<picture>
  <source srcSet="image-mobile.webp" media="(max-width: 600px)" />
  <source srcSet="image-desktop.webp" />
  <img src="image.jpg" alt="Fallback" />
</picture>

Bundle Size Analysis

bash

# Analyze bundle size
npm install -g webpack-bundle-analyzer
npm run build

# Identify large dependencies
npm ls

Security Best Practices

Secure code is responsible code.

Preventing XSS Attacks

javascript

// ❌ DANGEROUS: XSS vulnerability
function Comment({ text }) {
  return <div dangerouslySetInnerHTML={{ __html: text }} />;
}

// ✅ SAFE: React escapes HTML by default
function Comment({ text }) {
  return <div>{text}</div>;
}

Protecting Sensitive Data

javascript

// ❌ DANGEROUS: Token in localStorage
localStorage.setItem('token', authToken);

// ✅ BETTER: Token in httpOnly cookie (server-side)
// Cookie set with: 
// Set-Cookie: token=xyz; HttpOnly; Secure; SameSite=Strict

// ✅ Avoid storing sensitive data in state
// Use React Query or SWR with proper caching
import { useQuery } from '@tanstack/react-query';

function UserProfile() {
  const { data: user } = useQuery({
    queryKey: ['user'],
    queryFn: () => fetch('/api/user').then(res => res.json())
  });
}

Environment Variables

javascript

// ✅ Use environment variables for API keys
const API_KEY = process.env.REACT_APP_API_KEY;

// In .env file (NEVER commit to git)
REACT_APP_API_KEY=your_public_key_only

Advanced Patterns & Techniques

Compound Components Pattern

javascript

// Flexible, composable components
const Tabs = ({ children }) => {
  const [activeTab, setActiveTab] = useState(0);
  
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabsContext.Provider>
  );
};

Tabs.List = ({ children }) => <div className="tabs-list">{children}</div>;
Tabs.Tab = ({ index, children }) => {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  return (
    <button
      onClick={() => setActiveTab(index)}
      className={activeTab === index ? 'active' : ''}
    >
      {children}
    </button>
  );
};
Tabs.Panel = ({ index, children }) => {
  const { activeTab } = useContext(TabsContext);
  return activeTab === index ? children : null;
};

// Usage
<Tabs>
  <Tabs.List>
    <Tabs.Tab index={0}>Tab 1</Tabs.Tab>
    <Tabs.Tab index={1}>Tab 2</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel index={0}>Content 1</Tabs.Panel>
  <Tabs.Panel index={1}>Content 2</Tabs.Panel>
</Tabs>

Render Props Pattern

javascript

function Mouse({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (event) => {
    setPosition({ x: event.clientX, y: event.clientY });
  };

  return (
    <div onMouseMove={handleMouseMove}>
      {render(position)}
    </div>
  );
}

// Usage
<Mouse render={({ x, y }) => (
  <h1>The mouse is at ({x}, {y})</h1>
)} />

Common Mistakes to Avoid

❌ Missing Dependencies in useEffect

javascript

// ❌ BAD: Function runs infinite loop
function Profile({ userId }) {
  useEffect(() => {
    fetchUser(userId); // Missing userId in dependency array
  }, []); // BUG: Will only run once, ignores userId changes
}

// ✅ GOOD: Proper dependencies
function Profile({ userId }) {
  useEffect(() => {
    fetchUser(userId);
  }, [userId]); // Re-runs when userId changes
}

❌ Mutating State Directly

javascript

// ❌ BAD: Direct mutation
const [items, setItems] = useState([]);
items.push(newItem); // React won't detect change

// ✅ GOOD: Create new array
const [items, setItems] = useState([]);
setItems([...items, newItem]);

❌ Using Index as Key

javascript

// ❌ BAD: List keys should be unique
{items.map((item, index) => (
  <div key={index}>{item.name}</div> // BUG: Breaks reordering
))}

// ✅ GOOD: Use stable unique ID
{items.map(item => (
  <div key={item.id}>{item.name}</div>
))}

❌ Not Handling Loading/Error States

javascript

// ❌ BAD: Assumes data always exists
function UserList({ users }) {
  return <ul>{users.map(u => <li>{u.name}</li>)}</ul>;
}

// ✅ GOOD: Handle all states
function UserList({ users, loading, error }) {
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  if (!users?.length) return <p>No users found</p>;
  
  return <ul>{users.map(u => <li>{u.name}</li>)}</ul>;
}

FAQs

Q: Should I use class components or functional components?

A: Always use functional components with Hooks. Class components are legacy and shouldn’t be used for new code. React even plans to deprecate them long-term.

Q: When should I use Redux vs. Context API?

A:

  • Context API: App-wide state like theme, language, user authentication
  • Redux: Complex state logic, time-travel debugging, large teams, multiple reducers

Q: How much should I test?

A: Aim for 80% coverage of critical paths, 40-50% of general code. Testing everything is overkill; focus on:

  • User workflows
  • Error scenarios
  • Business logic

Q: Is TypeScript required?

A: No, but it’s highly recommended for:

  • Teams > 5 people
  • Large projects
  • APIs with many endpoints
  • Long-term maintenance

Q: How do I optimize bundle size?

A:

  1. Use code splitting (lazy import)
  2. Tree shake unused code
  3. Use production builds
  4. Audit dependencies (remove unused)
  5. Consider lighter alternatives (e.g., preact)

Q: What’s the best way to handle async data?

A: Use React Query or SWR:

javascript

import { useQuery } from '@tanstack/react-query';

function UserProfile() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user'],
    queryFn: () => fetch('/api/user').then(res => res.json())
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return <div>{data.name}</div>;
}

Q: How do I debug React applications?

A: Use React Developer Tools in Chrome:

  • Component tree inspection
  • Props/state viewing
  • Re-render profiling
  • Event listener debugging

Related React Learning Resources

Master React fundamentals by exploring the complete setup cluster:

Conclusion

React best practices are the foundation of professional development. By mastering these principles:

You write code others can maintain
You prevent security vulnerabilities
You build performant, accessible applications
You advance your career
You sleep better at night

Start with the fundamentals (functional components, hooks, state management) and gradually add complexity as your projects demand. Remember: The best code is code that can be maintained, tested, and scaled.

Picture of Priyanshu Pathak

Priyanshu Pathak

Priyanshu Pathak is a Senior Developer at Sourcebae. He works across the stack to build fast, reliable features that make hiring simple. From APIs and integrations to performance and security, Priyanshu keeps our products clean, scalable, and easy to maintain.

Table of Contents

Hire top 1% global talent now

FAQ

Still Curious? These might help

Related blogs

Quick answer Create routes in React using React Router v6 in 4 steps: That’s it. You now have a multi-page

Quick answer npm (Node Package Manager) is a package manager for JavaScript that manages all the libraries and tools your

Quick answer Install React JS in VS Code in 10 minutes: Your React dev environment is live and hot-reloading. Full

Quick Answer React is primarily a front-end technology. It is a JavaScript library designed for building user interfaces that run