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>
  );
}

Resources