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