Topics

On this page

Last updated on Nov 6, 2024

React Best Practices

To build efficient and maintainable React components in Gutenberg block development, it’s important to follow established best practices. Below are key practices extended with explanations and code examples.

Use Functional Components and Hooks over Class Components

Functional components are simpler and more concise than class components. They allow you to use React hooks for state management and side effects, leading to cleaner and more readable code. Additionally, functional components are the future of React development, with hooks providing powerful features that were previously only available in class components.

Example of a Functional Component with Hooks:

import { useState } from 'react';

const Counter = () => {

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

  const increment = () => {

    setCount(prevCount => prevCount + 1);

  };

  return (

    <div>

      <p>Count: {count}</p>

      <button onClick={increment}>Increment</button>

    </div>

  );

};

export default Counter;

Implement Proper State Management (useState, useReducer, useContext)

Use the appropriate hook for state management based on the complexity of your component:

import { useState } from 'react';

const ToggleSwitch = () => {

  const [isOn, setIsOn] = useState(false);

  const toggle = () => {

    setIsOn(prevState => !prevState);

  };

  return (

    <button onClick={toggle}>

      {isOn ? 'Switch Off' : 'Switch On'}

    </button>

  );

};

export default ToggleSwitch;

Example using useState:

import { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {

  switch (action.type) {

    case 'increment':

      return { count: state.count + 1 };

    case 'decrement':

      return { count: state.count - 1 };

    default:

      throw new Error();

  }

}

const Counter = () => {

  const [state, dispatch] = useReducer(reducer, initialState);

  return (

    <div>

      <p>Count: {state.count}</p>

      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>

      <button onClick={() => dispatch({ type: 'increment' })}>+</button>

    </div>

  );

};

export default Counter;

Example using useReducer:

Utilize Memoization (useMemo, useCallback) to Optimize Performance

Memoization helps to optimize performance by caching the result of expensive function calls and reusing them when the same inputs occur again. In React, useMemo memoizes the result of a function, and useCallback memoizes the function itself.

Example using useMemo:

import { useState, useMemo } from 'react';

const ExpensiveCalculationComponent = ({ num }) => {

  const [multiplier, setMultiplier] = useState(1);

  const expensiveCalculation = number => {

    console.log('Calculating...');

    // Simulate a CPU-intensive calculation

    return number  2;

  };

  const memoizedValue = useMemo(() => expensiveCalculation(num), [num]);

  return (

    <div>

      <p>Result: {memoizedValue * multiplier}</p>

      <button onClick={() => setMultiplier(multiplier + 1)}>

        Increase Multiplier

      </button>

    </div>

  );

};

export default ExpensiveCalculationComponent;

In this example, the expensive calculation only runs when num changes, not when multiplier changes.

Example using useCallback:

import { useState, useCallback } from 'react';

import List from './List';

const ParentComponent = () => {

  const [items, setItems] = useState([]);

  const fetchItems = useCallback(async () => {

    // Fetch items from an API or perform some action

    const newItems = await getItemsFromAPI();

    setItems(newItems);

  }, []);

  return (

    <div>

      <List fetchItems={fetchItems} />

    </div>

  );

};

export default ParentComponent;

By using useCallback, the fetchItems function is only recreated if its dependencies change, preventing unnecessary re-renders of the List component.

Break Down Complex Components into Smaller, Reusable Parts

Breaking down components improves readability, reusability, and testability. Smaller components are easier to manage and can be reused in different parts of your application.

Example:

Suppose you have a complex form component. You can break it down into smaller components like InputField, SelectField, and SubmitButton.

// InputField.js

const InputField = ({ label, value, onChange }) => (

  <div>

    <label>{label}</label>

    <input value={value} onChange={onChange} />

  </div>

);

export default InputField;

// SelectField.js

const SelectField = ({ label, options, value, onChange }) => (

  <div>

    <label>{label}</label>

    <select value={value} onChange={onChange}>

      {options.map(option => (

        <option key={option.value} value={option.value}>

          {option.label}

        </option>

      ))}

    </select>

  </div>

);

export default SelectField;

// SubmitButton.js

const SubmitButton = ({ onClick }) => (

  <button onClick={onClick}>Submit</button>

);

export default SubmitButton;

// ComplexForm.js

import { useState } from 'react';

import InputField from './InputField';

import SelectField from './SelectField';

import SubmitButton from './SubmitButton';

const ComplexForm = () => {

  const [name, setName] = useState('');

  const [option, setOption] = useState('');

  const handleSubmit = event => {

    event.preventDefault();

    // Handle form submission

  };

  return (

    <form onSubmit={handleSubmit}>

      <InputField label="Name" value={name} onChange={e => setName(e.target.value)} />

      <SelectField

        label="Options"

        value={option}

        onChange={e => setOption(e.target.value)}

        options={[

          { label: 'Option 1', value: '1' },

          { label: 'Option 2', value: '2' },

        ]}

      />

      <SubmitButton />

    </form>

  );

};

export default ComplexForm;

Use PropTypes or TypeScript for Better Type Checking

Using type checking helps catch bugs early and makes your code more predictable and easier to understand. PropTypes is a runtime type checking system, while TypeScript provides compile-time type checking.

Example using PropTypes:

import PropTypes from 'prop-types';

const Greeting = ({ name }) => <p>Hello, {name}!</p>;

Greeting.propTypes = {

  name: PropTypes.string.isRequired,

};

export default Greeting;

type GreetingProps = {

  name: string;

};

const Greeting: React.FC<GreetingProps> = ({ name }) => (

  <p>Hello, {name}!</p>

);

export default Greeting;

Avoid Prop Drilling by Using Context or State Management Libraries

Prop drilling occurs when you pass props through multiple levels of components that don’t need them, just to reach a deeply nested component. Using React Context or a state management library like Redux can help avoid this issue.

Example using React Context:

import { createContext, useContext } from 'react';

const UserContext = createContext();

const ParentComponent = () => {

  const user = { name: 'Alice' };

  return (

    <UserContext.Provider value={user}>

      <ChildComponent />

    </UserContext.Provider>

  );

};

const ChildComponent = () => <GrandchildComponent />;

const GrandchildComponent = () => {

  const user = useContext(UserContext);

  return <p>User: {user.name}</p>;

};

export default ParentComponent;

In this example, the user object is available in GrandchildComponent without passing it through ChildComponent.

Implement Error Boundaries to Catch and Handle Errors Gracefully

Error boundaries catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed.

Example of an Error Boundary:

import { Component } from 'react';

class ErrorBoundary extends Component {

  constructor(props) {

    super(props);

    this.state = { hasError: false };

  }

  static getDerivedStateFromError(error) {

    // Update state to render fallback UI

    return { hasError: true };

  }

  componentDidCatch(error, errorInfo) {

    // Log the error to an error reporting service

    logErrorToService(error, errorInfo);

  }

  render() {

    if (this.state.hasError) {

      // Render fallback UI

      return <h1>Something went wrong.</h1>;

    }

    return this.props.children;

  }

}

export default ErrorBoundary;

Usage:

<ErrorBoundary>

  <MyComponent />

</ErrorBoundary>

Use Keys Properly in Lists to Help React Identify Changes

Keys help React identify which items have changed, are added, or are removed. They should be given to elements inside an array to give the elements a stable identity.

Example:

const TodoList = ({ todos }) => (

  <ul>

    {todos.map(todo => (

      <li key={todo.id}>{todo.text}</li>

    ))}

  </ul>

);

In this example, todo.id is used as a key because it uniquely identifies each item.

Prefer Composition over Inheritance for Component Structure

Composition is a way of combining components where you include a component within another. It leads to more flexible and reusable code compared to inheritance.

Example:

const FancyBorder = ({ children }) => (

  &ltdiv className="fancy-border">

    {children}

  &lt/div>

);

const WelcomeDialog = () => (

  &ltFancyBorder>

    <h1>Welcome&lt/h1>

    <p>Thank you for visiting our spacecraft!&lt/p>

  &lt/FancyBorder>

);

export default WelcomeDialog;

Prefer Named Export

Named exports make it easier to refactor and import multiple components from a single file. They also improve the clarity of what is being imported.

Example:

// components.js

export const Button = () => {

  /* ... */

};

export const Input = () => {

  /* ... */

};

// Usage

import { Button, Input } from './components';

This is preferred over default exports when multiple exports are involved, as it provides better tooling support and makes the codebase more maintainable.

By following these React best practices with the provided examples, you can develop more efficient, maintainable, and scalable components within Gutenberg blocks, leading to better overall application performance and developer experience.


Contributor

Utsav Patel

Utsav

Utsav Patel

Software Engineer