Patterns for React Projects

A Guide to Building Scalable and Maintainable Applications

When building complex applications with React, one of the most important aspects is ensuring that the code remains maintainable, scalable, and easy to extend. As your app grows, managing complexity becomes challenging, and that’s where design patterns come into play. In this blog post, we will explore common design patterns in React and how they can help you write cleaner, more efficient, and more maintainable code.


List of content

  1. Container / Presentational Pattern
  2. Higher-Order Components (HOCs)
  3. Prop Combination
  4. Render Props
  5. Hooks
  6. Context API for Global State
  7. Smart / Dumb Components (Smart vs. Presentational)

React Patterns

Container / Presentational Pattern

This pattern separates the logic of your components from their visual presentation.

  • Presentational Components: These components are purely concerned with how things look. They receive data via props and render UI elements based on that data.
  • Container Components: These components are concerned with how data is fetched, transformed, and passed down to presentational components. They often contain business logic, manage state, and handle interactions.

By separating concerns between logic and UI, this pattern promotes reusability and keeps components focused on a single responsibility.

Example:

// Presentational Component
const UserList = ({ users }) => (
  <ul>
    {users.map(user => <li key={user.id}>{user.name}</li>)}
  </ul>
);

// Container Component
const UserListContainer = () => {
  const [users, setUsers] = useState([]);
  const fetchUsers = async () => {
    const response = await fetch('https://api.example.com/users');
    const data = await response.json();
    return data;
  };

  useEffect(() => {
    fetchUsers().then(fetchedUsers => setUsers(fetchedUsers));
  }, []); 

  return <UserList users={users} />;
};

export default UserListContainer;

Higher-Order Components (HOCs)

A Higher-Order Component (HOC) is a function that takes a component and returns a new component with enhanced functionality. HOCs are often used for adding cross-cutting concerns like authentication, logging, or handling lifecycle methods.

The key advantage of HOCs is that they allow you to reuse logic across multiple components without repeating code.

Example:

// HOC to add a title to a component
function withTitle(Component, title) {
  return function EnhancedComponent(props) {
    return (
      <>
        <h1>{title}</h1>
        <Component {...props} />
      </>
    );
  };
}
const Greeting = () => <p>Welcome to React!</p>;
const GreetingWithTitle = withTitle(Greeting, 'Hello World');
export default GreetingWithTitle;

Prop Combination

The Prop Combination pattern in React helps create flexible and reusable components by combining different props. It allows dynamic behavior based on various prop combinations without duplicating logic. Key benefits include:

  • Flexibility: Easily adjust component behavior by combining props.
  • Reusable Logic: Centralized logic for handling various prop combinations.
  • Dynamic UI: Change component appearance and behavior based on prop values.

Example: A button component can be enabled/disabled, show a loading state, and accept custom styles, all based on prop combinations.

Render Props Pattern

The Render Props pattern is used when you want to share logic between components without using HOCs. In this pattern, a component accepts a function as a prop (render prop) that allows it to pass data to the component it renders.

This is particularly useful when you need to encapsulate logic that can be reused across multiple components while keeping the component composition flexible.

Example:

import React, { useState, useEffect } from 'react';

const fetchData = async () => {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data;
};
const DataFetcher = ({ render }) => {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetchData().then(fetchedData => setData(fetchedData));
  }, []); 

  return render(data);
};
const App = () => (
  <DataFetcher render={data => <div>{data ? `Data: ${data}` : 'Loading...'}</div>} />
);

export default App;

State as a Hook

React introduced hooks in version 16.8, allowing functional components to manage state and side effects. The State as a Hook pattern promotes the use of the useState and useEffect hooks for handling local state and side effects in a clean, functional way.

Instead of using class components, you can implement state logic with hooks, which makes the code more concise and readable.

Example:

import React, { useState, useEffect } from 'react';

const Timer = () => {
  const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const interval = setInterval(() => setSeconds(prev => prev + 1), 1000);
    return () => clearInterval(interval);
  }, []);
  return <div>Time: {seconds} seconds</div>;
};

Context API for Global State

For complex applications that require global state (e.g., authentication, theming, user preferences), the Context API provides a way to share values across components without prop drilling.

The Context API allows you to avoid passing data through every level of the component tree, which can significantly simplify your application’s state management.

Example:

const ThemeContext = React.createContext();

const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

const ThemedComponent = () => {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
      <p>The current theme is {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
};

export const App = () => (
  <ThemeProvider>
    <ThemedComponent />
  </ThemeProvider>
);

Smart / Dumb Components (Smart vs. Presentational)

Similar to the Container/Presentational pattern, Smart/Dumb components distinguish between components that are responsible for logic (smart) and those that are responsible for rendering UI (dumb).

Smart components handle state management, data fetching, and other complex logic, while dumb components focus solely on rendering UI based on the props they receive.

This pattern is helpful for simplifying your component structure and improving the readability of your app.


Conclusion

Design patterns in React are not just a way to improve your code’s structure—they are tools that can help you scale your application and manage complexity as it grows. Whether you are working with functional components and hooks or class components and HOCs, understanding and applying these patterns will allow you to write cleaner, more maintainable code.

By following these patterns, you can build applications that are easier to extend and test while reducing the amount of redundant code in your codebase. And remember, there’s no one-size-fits-all pattern—use the right one for the right situation!

Do you have a favorite React design pattern? Let us know in the comments below!

Publicar comentário