← Back to blog

Building Modern React Applications: Patterns and Practices

12 min readBy Riku Rainio
ReactJavaScriptBest PracticesArchitecture

Building Modern React Applications: Patterns and Practices

React has evolved significantly over the years, and with it, the patterns and practices for building applications have matured. In this article, we'll explore modern approaches to building React applications that are scalable, maintainable, and performant.

The Evolution of React Patterns

React has gone through several paradigm shifts:

  1. Class ComponentsFunctional Components
  2. Lifecycle MethodsHooks
  3. Prop DrillingContext API & State Management
  4. Client-Side RenderingServer Components

Modern Component Architecture

Component Composition

One of the most powerful patterns in React is component composition. Instead of creating monolithic components, break them down into smaller, reusable pieces.

// Bad: Monolithic component
function UserProfile({ user }) {
  return (
    <div>
      <div className="header">
        <img src={user.avatar} />
        <h1>{user.name}</h1>
      </div>
      <div className="content">
        <p>{user.bio}</p>
        <button>Follow</button>
      </div>
    </div>
  )
}

// Good: Composed components
function UserProfile({ user }) {
  return (
    <Card>
      <UserHeader user={user} />
      <UserContent user={user} />
      <UserActions user={user} />
    </Card>
  )
}

Custom Hooks for Logic Reuse

Custom hooks are a powerful way to extract and reuse logic:

function useUserData(userId) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    fetchUser(userId)
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false))
  }, [userId])

  return { user, loading, error }
}

// Usage
function UserProfile({ userId }) {
  const { user, loading, error } = useUserData(userId)
  
  if (loading) return <Spinner />
  if (error) return <ErrorMessage error={error} />
  return <UserCard user={user} />
}

State Management Strategies

When to Use Local State

For component-specific state that doesn't need to be shared:

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

When to Use Context

For state that needs to be shared across multiple components:

const ThemeContext = createContext()

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

When to Use External State Management

For complex applications with lots of shared state, consider libraries like:

  • Zustand - Lightweight and simple
  • Jotai - Atomic state management
  • Redux Toolkit - For complex state logic

Performance Optimization

React.memo for Expensive Components

Use React.memo to prevent unnecessary re-renders:

const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
  // Expensive computation
  const processed = useMemo(() => {
    return heavyComputation(data)
  }, [data])
  
  return <div>{processed}</div>
})

Code Splitting with React.lazy

Split your code to reduce initial bundle size:

const HeavyComponent = React.lazy(() => import('./HeavyComponent'))

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <HeavyComponent />
    </Suspense>
  )
}

TypeScript Best Practices

TypeScript adds type safety to React applications:

interface User {
  id: string
  name: string
  email: string
}

interface UserCardProps {
  user: User
  onEdit?: (user: User) => void
}

function UserCard({ user, onEdit }: UserCardProps) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      {onEdit && <button onClick={() => onEdit(user)}>Edit</button>}
    </div>
  )
}

Testing Strategies

Component Testing

Test components in isolation:

import { render, screen } from '@testing-library/react'
import { UserCard } from './UserCard'

test('renders user information', () => {
  const user = { id: '1', name: 'John', email: 'john@example.com' }
  render(<UserCard user={user} />)
  
  expect(screen.getByText('John')).toBeInTheDocument()
  expect(screen.getByText('john@example.com')).toBeInTheDocument()
})

Conclusion

Building modern React applications requires understanding these patterns and practices. By focusing on component composition, proper state management, performance optimization, and type safety, you can create applications that are both maintainable and scalable.

Remember: the best pattern is the one that fits your project's needs. Start simple and add complexity only when necessary.