myesn

myEsn2E9

hi
github

15 Principles and Best Practices for React Components to Achieve Better Software Architecture and Design (Translation and Reprint)

Original Address#

https://thetshaped.dev/p/15-react-component-principles-for-better-design

This article only translates the main content, and of course, this is all thanks to Microsoft Translator and ChatGPT (GPT-4o).

Introduction#

This series of articles aims to bridge the gap between React beginners and those who grow into React experts and engineers.

Caution

This is not a beginner's guide, so most of the concepts shared require some knowledge of React.

Tip

Treat everything as an opinion. Software can be built in many ways.

1. Prefer Function Components Over Class Components#

Class components can be verbose and harder to manage. Function components are simpler and easier to understand. Using function components gives you better readability. Compared to state management, lifecycle methods, etc., in class components, there are far fewer things to remember and think about.

The only exception for using class components is for error boundaries.

⛔ Avoid using class components

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  
  increment() {
    this.setState(state => ({
      count: state.count + 1
    }));
  }

  return (
    <div>
      Count: {this.state.count}
      <button onClick={() => this.increment()}>Increment</button>
    </div>
  );
}

✅ Prefer using function components

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      Count: {count}
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

2. Name Your Components#

Unnamed components can make debugging difficult and reduce code readability. Named components improve stack traces and make your codebase easier to navigate, manage, and understand. When using named components, it is easier to navigate between errors.

⛔ Avoid using unnamed components

export default () => <div>Details</div>;

✅ Prefer named components

export default function UserDetails() {
  return <div>User Details</div>;
}

3. Move Helper Functions Outside of Components#

Nested helper functions inside components can make the component cluttered and harder to read. Placing helper functions outside of components improves readability and separates concerns.

⛔ Avoid nesting helper functions inside components if closures are not needed

function UserProfile({ user }) {
  function formatDate(date) {
    return date.toLocaleDateString();
  }
  return <div>Joined: {formatDate(user.joinDate)}</div>;
}

✅ Prefer moving these helper functions outside of the component (place them before the component so you can read the file from top to bottom)

function formatDate(date) {
  return date.toLocaleDateString();
}

function UserProfile({ user }) {
  return <div>Joined: {formatDate(user.joinDate)}</div>;
}

4. Use Configuration Objects to Extract Repeated Markup#

Hardcoding repeated markup makes the code harder to maintain and update. Using mapping/loops and configuration objects to extract repeated markup makes the code easier to maintain and read. It simplifies updates and additions since changes only need to be made in one place (within the configuration object).

⛔ Avoid hardcoding repeated markup

function ProductList() {
  return (
    <div>
      <div>
        <h2>Product 1</h2>
        <p>Price: $10</p>
      </div>
      <div>
        <h2>Product 2</h2>
        <p>Price: $20</p>
      </div>
      <div>
        <h2>Product 3</h2>
        <p>Price: $30</p>
      </div>
    </div>
  );
}

✅ Prefer using configuration objects and loops to extract repeated markup

const products = [
  { id: 1, name: 'Product 1', price: 10 },
  { id: 2, name: 'Product 2', price: 20 },
  { id: 3, name: 'Product 3', price: 30 }
];
  
function ProductList() {
  return (
    <div>
      {products.map(product => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>Price: ${product.price}</p>
        </div>
      ))}
    </div>
  );
}

5. Manage Component Size#

Large and verbose components can be difficult to understand, maintain, and test. Smaller, more focused components are easier to read, test, and maintain. Each component should have a single responsibility, which is the reason for changes and re-renders, making the codebase more modular and easier to manage.

⛔ Avoid using large and unwieldy components

function UserProfile({ user }) {
  return (
    <div>
      <div>
        <img src={user.avatar} alt={`${user.name}'s avatar`} />
        <h2>{user.name}</h2>
      </div>
      <div>
        <h3>Contact</h3>
        <p>Email: {user.email}</p>
        <p>Phone: {user.phone}</p>
      </div>
    </div>
  );
}

✅ Prefer small and readable components

function UserProfile({ user }) {
  return (
    <div>
      <ProfileHeader avatar={user.avatar} name={user.name} />
      <ProfileContact email={user.email} phone={user.phone} />
    </div>
  );
}

6. Destructure Props#

Repetitive props can make components harder to read and maintain. Destructuring props can improve readability and make the code more concise. It reduces repetition and clearly indicates which props are being used.

⛔ Avoid repeating props everywhere in the component

function UserProfile(props) {
  return (
    <>
      <div>Name: {props.name}</div>
      <div>Email: {props.email}</div>
    </>
  );
}

✅ Prefer destructuring your props

function UserProfile({ name, email }) {
  return (
    <>
      <div>Name: {name}</div>
      <div>Email: {email}</div>
    </>
  );
}

7. Manage the Number of Props#

Having too many props can make a component complex and hard to understand. Fewer props make components easier to use and understand.

Most of the time, when we have a component with > 5 props, it indicates that it can be split. But this is not a hard and fast rule, as a "good" example is that an input field has many props but does not need to be split.

When we have < 5 props, it indicates that something can be extracted. Perhaps we have included too much data in a single component.

Important

Fewer props ⇒ fewer reasons for changes and re-renders.

⛔ Avoid using many props (perhaps > 5, you should split it, but not always, e.g., input)

function UserProfile({ 
  name, email, avatarUrl, address, paymentProfiles 
}) {
  return (
    <div>
      <img src={avatarUrl} alt={`${name}'s avatar`} />
      <h1>{name}</h1>
      <p>Email: {email}</p>
      <p>Address: {address}</p>
      <ul>
        {paymentProfiles.map(paymentProfile => (
          <li key={paymentProfile.id}>
            <h2>{paymentProfile.cardNumber}</h2>
            <p>{paymentProfile.cardName}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

✅ Prefer using a small number of props (possibly < 5)

function UserProfile({ user }) {
  return (
    <Info name={user.name} email={user.email} avatarUrl={user.avatarUrl} />
    <Address address={user.address} />
    <PaymentProfiles paymentProfiles={user.paymentProfiles} />
  );
}

8. Props - Objects vs Primitives#

Passing many primitives can make components cluttered and make managing related data more difficult. Grouping related props into an object can simplify the component interface and improve readability. It makes the code clearer and easier to understand by logically grouping related data.

⛔ Avoid passing primitives when props are related

function Address({ street, city, state, zip }) {
  return (
    <div>
      <p>Street: {street}</p>
      <p>City: {city}</p>
      <p>State: {state}</p>
      <p>ZIP: {zip}</p>
    </div>
  );
}

✅ Prefer passing an object to group props

function Address({ address }) {
  const { street, city, state, zip } = address;

  return (
    <div>
      <p>Street: {street}</p>
      <p>City: {city}</p>
      <p>State: {state}</p>
      <p>ZIP: {zip}</p>
    </div>
  );
}

9. Manage Ternary Operators#

Nested ternary operators can make code hard to read and maintain. Clear if-else statements can improve code readability and maintainability, making control flow easier to understand and debug.

⛔ Avoid nested or multiple ternary operators - hard to read and follow

function Greeting({ isLoggedIn, age }) {
  return (
    <div>
      {isLoggedIn ? (
        age > 18 ? (
          "Welcome back!"
        ) : (
          "You are underaged!"
        )
      ) : (
        "Please log in."
      )}
    </div>
  );
}

✅ Prefer if-else blocks and explicit return statements in components

function Greeting({ isLoggedIn, age }) {
  if (!isLoggedIn) {
    return <div>Please log in.</div>;
  }

  if (age > 18) {
    return <div>Welcome back!</div>;
  }

  return <div>You are underaged!</div>;
}

10. Abstract List Mapping to Separate Components#

Mapping lists directly in the return statement can make components appear cluttered and hard to read. Separating the map operation from the main component into a separate component can make the code cleaner and more readable. The boilerplate code of the main component becomes simpler, and separating the rendering logic from the main structure of the component enhances readability.

Note

The main component does not care about the details.

⛔ Avoid using the map function on lists within components

function PaymentProfilesPage({ paymentProfiles }) {
  return (
    <h1>Payment Profiles:</h1>
    <ul>
      {paymentProfiles.map(paymentProfile => (
        <li key={paymentProfile.id}>
          <h2>{paymentProfile.cardNumber}</h2>
          <p>{paymentProfile.cardName}</p>
        </li>
      ))}
    </ul>
  );
}

✅ Prefer moving the map function outside the component - easier to read. The main component does not care about the details.

function PaymentProfilesPage({ paymentProfiles }) {
  return (
    <h1>Payment Profiles:</h1>
    <PaymentProfilesList paymentProfiles={paymentProfiles} />
  );
}

11. Prefer Using Hooks Over HOCs and Render Props#

Higher-order components (HOCs) and render props patterns have traditionally been used to share logic and behavior between components. However, these patterns can lead to complex and deeply nested component trees, making the code harder to read, debug, and maintain. Hooks provide a more concise and declarative way to encapsulate and reuse logic in function components.

Hooks offer a simpler mental model - we access external logic and behavior by composing a set of functions, making the entire JSX template easier to read and understand.

⛔ Avoid using HOCs and render props

function UserProfileForm() {
  return (
    <Form>
      {({ values, handleChange }) => (
        <input
          value={values.name}
          onChange={e => handleChange('name', e.target.value)}
        />
        <input
          value={values.password}
          onChange={e => handleChange('password', e.target.value)}
        />
      )}
    </Form>
  );
}

✅ Prefer using hooks

function UserProfileForm() {
  const { values, handleChange } = useForm();

  return (
    <Form>
      <input
        value={values.name}
        onChange={e => handleChange('name', e.target.value)}
      />
      <input
        value={values.password}
        onChange={e => handleChange('password', e.target.value)}
      />
    </Form>
  );
}

12. Use Custom Hooks to Reuse and Encapsulate Logic#

Duplicated logic can lead to code redundancy and make maintenance more difficult. Custom hooks allow for code reuse, making components cleaner and easier to maintain. By using custom hooks, you can encapsulate logic, reducing duplication and improving readability. It also makes testing easier.

⛔ Avoid repeating logic in multiple components

function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch('/api/users')
      .then(response => response.json())
      .then(data => setUsers(data));
  }, []);

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

function ProductList() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetch('/api/products')
      .then(response => response.json())
      .then(data => setProducts(data));
  }, []);

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

✅ Prefer using custom hooks to encapsulate and reuse logic

function useFetch(url) {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch(url)
      .then(response => response.json())
      .then(data => setData(data));
  }, [url]);

  return data;
}

function UserList() {
  const users = useFetch('/api/users');

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

function ProductList() {
  const products = useFetch('/api/products');

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

13. Extract Render Functions#

Nesting complex render functions inside components can make the component appear cluttered and hard to read, test, and maintain. Defining render functions outside of the component or using separate components can improve readability and maintainability. This practice keeps the main component clean and focused on its primary functionality.

⛔ Avoid nesting render functions inside components

function UserProfile({ user }) {
  function renderProfileDetails() {
    return <div>{user.name} - {user.age}</div>;
  }

  return <div>{renderProfileDetails()}</div>;
}

✅ Prefer extracting render functions outside of the component - above the component or in a separate component.

function renderProfileDetails(user) {
  return <div>{user.name} - {user.age}</div>;
}

// OR using a separate component
function ProfileDetails({ user }) {
  return <div>{user.name} - {user.age}</div>;
}

function UserProfile({ user }) {
  return (
    <div>
      {renderProfileDetails(user)}
      // OR
      <ProfileDetails user={user} />;
    </div>
  );
}

14. Use Error Boundaries#

Uncaught errors can cause the entire application to crash, affecting user experience. Error boundaries allow you to gracefully catch and handle errors, improving the resilience of the application. This ensures a better user experience by displaying a fallback UI instead of crashing the entire application.

⛔ Avoid letting errors in child components crash the entire application

function App() {
  return <UserProfile />;
}

✅ Prefer using error boundaries to catch and handle errors in the child component tree

function App() {
  return (
    <ErrorBoundary>
      <UserProfile />
    </ErrorBoundary>
  );
}

15. Use Suspense#

Manually managing loading states can be repetitive and error-prone. Suspense simplifies handling loading states by providing a declarative way to manage asynchronous operations. This reduces boilerplate code and makes component logic cleaner.

⛔ Avoid manually handling loading states for asynchronous operations

function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/user')
      .then(response => response.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, []);

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

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

✅ Prefer using Suspense to elegantly handle asynchronous operations and loading states

import { UserProfile } from './UserProfile';

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile />
    </Suspense>
  );
}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.