上回说道在 React アプリケーションでリストコンポーネントは最適化すべきで、今日は複雑なコンポーネントをどう書くかについてです。Jotai と Zustand が咕咕咕したので、次回にします。
大手企業の屎山を経験したことがある人は、コンポーネントが数百行、さらには千行を超えることがよくあることを知っているでしょう。コンポーネント内部に別のコンポーネントをネストするのも一般的です。簡単に三つの「しないこと」をまとめました:
- コンポーネント内部で他のコンポーネントを定義しない
- render の方法で ReactNode をレンダリングしない、例:
render(data1, data2, data3) => ReactNode
- useCallback でコンポーネントを定義しない
何を意味するのか、上記の誤った書き方を簡単に列挙しますので、皆さんは絶対に真似しないでください:
// ❌ コンポーネント内部で他のコンポーネントを定義
function ParentComponent(props) {
function ChildComponent() {
return <div>私は子コンポーネントです</div>
}
return (
<div>
<ChildComponent />
</div>
)
}
// ❌ render の方法で ReactNode をレンダリング:
function MyComponent({ data1, data2, data3 }) {
const render = (data1, data2, data3) => {
return <div>{data1}{data2}{data3}</div>
}
return render(data1, data2, data3);
}
// ❌ useCallback でコンポーネントを定義
import React, { useCallback } from 'react';
function ParentComponent(props) {
const ChildComponent = useCallback(() => {
return <div>私は子コンポーネントです</div>
}, []);
return (
<div>
<ChildComponent />
</div>
)
}
以上は非常に単純な例ですが、こうした誤りを犯す人はいないでしょう。なぜなら、このコンポーネントは非常にシンプルで、簡単に分割できるからです。どうしてこんなに初心者のコードを書くことができるのでしょうか。実際のビジネスシーンでは、コンポーネントの複雑さはこれよりもはるかに大きく、時にはロジックを抽出することができず、こうした初心者のコードを書くしかないこともあります。その結果、すべての状態がコンポーネントのトップレベルにあり、コンポーネント内のコンポーネントもトップレベルコンポーネントの再レンダリングによって常に再構築され、パフォーマンスが非常に低下します。
コンポーネント内部での他のコンポーネントの定義を排除する#
少し複雑な、データが結合されたビジネスコンポーネントを定義しなければならない場合、コンポーネント内に位置するコンポーネントを使用する必要があるかもしれません。そして、それを最適化します。
例えば、OrderForm
コンポーネントがあり、渡された注文情報に基づいて異なる OrderItem
コンポーネントを動的に生成する必要があるとします。この時、OrderForm
内に OrderItem
コンポーネントを定義したくなるかもしれません。以下のように:
誤った書き方:
function OrderForm({ orders }) {
const [myName, setMyName] = useState('Foo')
function OrderItem({ order }) {
return (
<li>
<h2>{order.name}</h2>
<p>数量: {order.quantity}</p>
<p>単価: {order.price}</p>
<p>{myName}</p>
</li>
)
}
// または以下の形式
const OrderItem = React.useCallback(({ order }) => {
return (
<li>
<h2>{order.name}</h2>
<p>数量: {order.quantity}</p>
<p>単価: {order.price}</p>
<p>{myName}</p>
</li>
)
}, [order, myName])
return (
<ul>
{orders.map(order =>
<OrderItem key={order.id} order={order} />
)}
</ul>
);
}
しかし、こうすると、OrderForm
がレンダリングされるたびに OrderItem
が再構築され、すべての OrderItem
が再マウントされることになります。React では、新しいコンポーネントインスタンス(つまりコンポーネントの初回レンダリング)を作成することは、既存のコンポーネントインスタンス(つまりコンポーネントの再レンダリング)を更新するよりも通常は時間がかかります。
正しい方法は、OrderItem
コンポーネントを OrderForm
の外部に定義し、props を通じてデータを渡すことです:
function OrderItem({ order, myName }) {
return (
<li>
<h2>{order.name}</h2>
<p>数量: {order.quantity}</p>
<p>単価: {order.price}</p>
<p>{myName}</p>
</li>
)
}
function OrderForm({ orders }) {
return (
<ul>
{orders.map(order =>
<OrderItem key={order.id} order={order} myName={myName}/>
)}
</ul>
);
}
こうすることで、OrderForm
がどのようにレンダリングされても、OrderItem
は一度だけ定義され、props の変更に応じて再レンダリングが必要かどうかを判断できるため、パフォーマンスが向上します。
render 方法での引数渡しのコンポーネントを排除する#
時々、コンポーネントがこのようにスロットを定義しているのを見かけます。例えば、以下の Table コンポーネントです。
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>
);
};
上記の書き方もあまり良くありません。まず、render は関数が返す ReactNode なので、このコンポーネントを使用する際に渡す render では React Hooks を使用できません。例えば:
<Table data={[]} columnRenderers={[
(data, index) => {
useState() // ❌ 使用できない
return <div>{data.id}</div>
}
]}
Hooks を使用できないだけでなく、このような書き方は Table コンポーネントが再レンダリングされた後、すべての列の render が再構築される原因になります。なぜなら、render はコンポーネントではなく ReactNode であり、パフォーマンス最適化に使用できないからです。
上記の Table は次のように書き換えることができます:
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>
);
};
こうすることで、列コンポーネントを定義する際に Hooks を使用でき、列コンポーネントは data
の影響のみを受けます(前提として列コンポーネントに memo を適用する必要があります)。また、元の (data1, data2) =>
を ({ data1, data2 })
に書き換えるだけで済みます。
例えば:
<Table data={[]} columnComponents={[
ColumnComponent1
]}
const ColumnComponent1 = memo(({ data, index }) => {
return <div>{data.id}</div>
})
今日はここまでにします。
上記の部分コードと文字は GPT-4 によって作成されました。
この記事は Mix Space によって xLog に同期更新されました。原始リンクは https://innei.ren/posts/programming/experience-in-performance-optimization-in-react-applications-2