DEV Community

Ugo VENTURE
Ugo VENTURE

Posted on

Part 2: Clean Design in React

Table of Contents:

Clean design in React applications is essential for maintaining readability, reusability, and scalability. As applications grow in complexity, adhering to good design principles becomes crucial for developers. In this article, we will explore the significance of component composition, examine effective React design patterns, and delve into the power of declarative programming. By adopting these practices, developers can create applications that are not only functional but also elegant and easy to maintain.

Component Composition

Component composition is the cornerstone of building modular and reusable React applications. It allows developers to create complex UIs by combining smaller, simpler components, each with a specific purpose.

Example:

Consider a Button component

import React from 'react';

type ButtonProps = {
  children: React.ReactNode;
  onClick: () => void;
};

const Button: React.FC<ButtonProps> = ({ children, onClick }) => (
  <button onClick={onClick}>{children}</button>
);
Enter fullscreen mode Exit fullscreen mode

Now, you can compose this button within different contexts:

const SubmitButton: React.FC = () => (
  <Button onClick={() => console.log('Submitted!')}>Submit</Button>
);

const CancelButton: React.FC = () => (
  <Button onClick={() => console.log('Cancelled!')}>Cancel</Button>
);
Enter fullscreen mode Exit fullscreen mode

This approach reduces duplication and enhances readability. When you compose components, you create a clear hierarchy and a flow that mirrors the UI structure, making it easier to understand the relationships between different parts of your application.

React Design Patterns

When building React applications, design patterns play a crucial role in ensuring scalability, maintainability, and clean design. While there are many patterns used by developers to structure their React code, we won’t cover all of them in this article. Instead, we’ll focus on a few of the most commonly used and impactful patterns to get you started.

For a more comprehensive deep dive into all the React design patterns, stay tuned for a future article where I’ll cover the full range of patterns that can be applied in different scenarios.

Presentational and Container Components

This pattern separates the UI (presentational components) from the logic (container components). Presentational components are responsible for rendering UI, while container components handle state and business logic.

Example:

// Presentational Component
type UserListProps = {
  users: { id: number; name: string }[];
};

const UserList: React.FC<UserListProps> = ({ users }) => (
  <ul>
    {users.map(user => (
      <li key={user.id}>{user.name}</li>
    ))}
  </ul>
);

// Container Component
const UserContainer: React.FC = () => {
  const [users, setUsers] = React.useState<{ id: number; name: string }[]>([]);

  React.useEffect(() => {
    const fetchUsers = async () => {
      const response = await fetch('/api/users');
      const data = await response.json();
      setUsers(data);
    };

    fetchUsers();
  }, []);

  return <UserList users={users} />;
};
Enter fullscreen mode Exit fullscreen mode

This separation promotes better maintainability and testing.

Higher-Order Components (HOCs)

HOCs are functions that take a component and return a new component, enhancing its functionality. They are useful for code reuse, such as adding authentication or data fetching capabilities.

Example:

import React from 'react';

const withUserData = <P extends object>(WrappedComponent: React.ComponentType<P>) => {
  return (props: Omit<P, 'user'>) => {
    const [user, setUser] = React.useState<{ id: number; name: string } | null>(null);

    React.useEffect(() => {
      const fetchUser = async () => {
        const response = await fetch('/api/user');
        const data = await response.json();
        setUser(data);
      };

      fetchUser();
    }, []);

    return <WrappedComponent user={user} {...(props as P)} />;
  };
};

// Usage
const UserProfile: React.FC<{ user: { id: number; name: string } | null }> = ({ user }) => {
  return <div>{user ? `Hello, ${user.name}` : 'Loading...'}</div>;
};

const EnhancedUserProfile = withUserData(UserProfile);
Enter fullscreen mode Exit fullscreen mode

Render Props

This pattern allows sharing code between components using a prop that is a function. It enhances flexibility in rendering logic.

Example:

import React from 'react';

type DataFetcherProps = {
  render: (data: any) => React.ReactNode;
};

const DataFetcher: React.FC<DataFetcherProps> = ({ render }) => {
  const [data, setData] = React.useState<any>(null);

  React.useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('/api/data');
      const result = await response.json();
      setData(result);
    };

    fetchData();
  }, []);

  return <>{render(data)}</>;
};

// Usage
<DataFetcher render={(data) => <DisplayComponent data={data} />} />;
Enter fullscreen mode Exit fullscreen mode

Custom Hooks

Custom hooks allow you to encapsulate reusable logic in a function that can be shared across components, promoting cleaner and more organized code.

Example:

import { useState, useEffect } from 'react';

const useFetch = (url: string) => {
  const [data, setData] = useState<any>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(url);
      const result = await response.json();
      setData(result);
      setLoading(false);
    };

    fetchData();
  }, [url]);

  return { data, loading };
};

// Usage in a component
const DataDisplay: React.FC = () => {
  const { data, loading } = useFetch('/api/data');

  if (loading) {
    return <div>Loading...</div>;
  }

  return <div>{JSON.stringify(data)}</div>;
};
Enter fullscreen mode Exit fullscreen mode

Declarative Programming

Declarative programming focuses on what to achieve rather than how to achieve it. React embraces this paradigm by allowing developers to describe the UI in terms of its state rather than the steps to update the UI.

With React’s declarative nature, UI updates occur seamlessly as the state changes. This leads to less code, improved readability, and "fewer" bugs.

Example:

import React, { useState } from 'react';

const App: React.FC = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this example, React handles the DOM updates, allowing developers to focus on the application logic rather than manipulating the DOM directly.

Best Practices for Clean Design

To maintain clean design in React applications, consider the following best practices:

  • DRY (Don’t Repeat Yourself): Avoid code duplication by creating reusable components and hooks.
  • KISS (Keep It Simple, Stupid): Keep components focused and simple. If a component becomes too complex, consider breaking it down.
  • Type Safety: Use TypeScript to provide type safety and improve the developer experience.
  • Testing: Write tests for your components to ensure they behave as expected. This includes unit tests and integration tests.

Conclusion

Embracing clean design principles in React applications enhances maintainability, readability, and scalability. By focusing on component composition, utilizing effective design patterns, and leveraging declarative programming, developers can create robust applications that stand the test of time. Adopting these practices will lead to a more enjoyable and efficient development experience.

Top comments (0)