banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

Experience in Performance Optimization in React Applications (Part 2)

Last time we talked about optimizing list components in React applications, and today let's talk about how to write complex components. Jotai and Zustand are still pending, we'll talk about them next time.

Those who have worked in big companies should have experienced having components with hundreds or even thousands of lines. It's also common to nest one component inside another. Here are three things to avoid:

  • Avoid defining other components inside a component.
  • Avoid rendering ReactNode using the render method, e.g. render(data1, data2, data3) => ReactNode.
  • Avoid using useCallback to define a component.

What does this mean? Let's list some examples of the incorrect practices mentioned above, and please don't learn from them:

// ❌ Defining other components inside a component
function ParentComponent(props) {
    function ChildComponent() {
        return <div>I am a child component</div>
    }

    return (
        <div>
            <ChildComponent />
        </div>
    )
}

// ❌ Rendering ReactNode using the render method:
function MyComponent({ data1, data2, data3 }) {
    const render = (data1, data2, data3) => {
        return <div>{data1}{data2}{data3}</div>
    }

    return render(data1, data2, data3);
}

// ❌ Using useCallback to define a component
import React, { useCallback } from 'react';

function ParentComponent(props) {
    const ChildComponent = useCallback(() => {
        return <div>I am a child component</div>
    }, []);

    return (
        <div>
            <ChildComponent />
        </div>
    )
}

The above examples are very simple, and no one would make such novice mistakes because the components are simple and easy to split. How could anyone write such beginner code? In actual business scenarios, the complexity of components is much greater than this. Sometimes it seems impossible to extract some logic, so we can only write such beginner code, resulting in all states being at the top level of the component. The components inside the component will also be constantly rebuilt due to the re-render of the top-level component, resulting in very poor performance.

Eliminating the Definition of Other Components Inside a Component#

Let's define a slightly more complex business component that uses components inside components and optimize it.

Suppose we have an OrderForm component that needs to dynamically generate different OrderItem components based on the passed-in order information. At this point, you may want to define the OrderItem component inside OrderForm, like this:

Incorrect approach:

function OrderForm({ orders }) {
    const [myName, setMyName] = useState('Foo')
    function OrderItem({ order }) {
        return (
            <li>
                <h2>{order.name}</h2>
                <p>Quantity: {order.quantity}</p>
                <p>Price: {order.price}</p>
                <p>{myName}</p>
            </li>
        )
    }
    
    // Or in the following form
    const OrderItem = React.useCallback(({ order }) => {
        return (
            <li>
                <h2>{order.name}</h2>
                <p>Quantity: {order.quantity}</p>
                <p>Price: {order.price}</p>
                <p>{myName}</p>
            </li>
        )
    }, [order, myName])

    return (
        <ul>
            {orders.map(order => 
                <OrderItem key={order.id} order={order} />
            )}
        </ul>
    );
}

But by doing this, every time OrderForm is rendered, OrderItem will be recreated, which means that all OrderItem components in the list will be remounted instead of re-rendered. In React, creating a new component instance (i.e., the initial rendering of a component) usually takes more time than updating an existing component instance (i.e., re-rendering a component).

The correct approach is to define the OrderItem component outside of OrderForm and pass the data through props:

function OrderItem({ order, myName }) {
    return (
        <li>
            <h2>{order.name}</h2>
            <p>Quantity: {order.quantity}</p>
            <p>Price: {order.price}</p>
            <p>{myName}</p>
        </li>
    )
}

function OrderForm({ orders }) {
    return (
        <ul>
            {orders.map(order => 
                <OrderItem key={order.id} order={order} myName={myName}/>
            )}
        </ul>
    );
}

This way, no matter how OrderForm is rendered, OrderItem will only be defined once, and it can decide whether to re-render based on changes in props, thus improving performance.

Removing Components Rendered Using the Render Method#

Sometimes you may come across components that define slots in this way. For example, the Table component below.

type Data = (string | number)[][];

type ColumnRenderer = (data: string | number, rowIndex: number, columnIndex: number) => React.ReactNode;

interface TableProps {
    data: Data;
    columnRenderers: ColumnRenderer[];
}

const Table: React.FC<TableProps> = ({ data, columnRenderers }) => {
    return (
        <table>
            <tbody>
                {data.map((row, rowIndex) => (
                    <tr key={rowIndex}>
                        {columnRenderers.map((render, columnIndex) => (
                            <td key={columnIndex}>
                                {render(row[columnIndex], rowIndex, columnIndex)}
                            </td>
                        ))}
                    </tr>
                ))}
            </tbody>
        </table>
    );
};

The above approach is not ideal. First, the render function returns a ReactNode, so when you use this component, the render passed in cannot use React Hooks. For example:

<Table data={[]} columnRenderers={[
    (data, index) => {
      useState() // ❌ Cannot use
      return <div>{data.id}</div>
    }
]}

Not only can you not use Hooks, but this approach will cause all the columns to be re-created after the Table component is re-rendered. This is because render is not a component but a ReactNode, so it cannot be used for performance optimization.

The above Table can be rewritten as:

type Data = (string | number)[][];

interface TableColumnProps {
    data: string | number;
    render: (data: string | number) => React.ReactNode;
}

interface TableProps {
    data: Data;
    columnComponents: Array<React.FC<TableColumnProps>>;
}

const Table: React.FC<TableProps> = ({ data, columnComponents }) => {
    return (
        <table>
            <tbody>
                {data.map((row, rowIndex) => (
                    <tr key={rowIndex}>
                        {columnComponents.map((Component, columnIndex) => (
                            <Component key={columnIndex} data={row[columnIndex]} />
                        ))}
                    </tr>
                ))}
            </tbody>
        </table>
    );
};

This way, you can use Hooks when defining column components, and the column components will only be affected by data (assuming you wrap the column components with memo), and you only need to change (data1, data2) => to ({ data1, data2 }).

For example:

<Table data={[]} columnComponents={[
   ColumnComponent1
]}

const ColumnComponent1 = memo(({ data, index }) => {
      return <div>{data.id}</div>
})

That's all for today.

The above code and text were written by GPT-4.

This article is synchronized and updated to xLog by Mix Space.
The original link is https://innei.ren/posts/programming/experience-in-performance-optimization-in-react-applications-2


Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.