Reactを使って開発をしている中でバグ修正に長時間費やしてしまったので、今回の件を備忘録として書き残しておきます。
今回作りたい機能
テンプレートにフィルターを掛けたものに対してmapメソッドを使用して一つひとつ子コンポーネントを作成する機能
問題の箇所
当初、useEffectを使用すれば必要なタイミングにのみフィルター機能が動作すると考えていました。
実行タイミング
- テンプレートが更新された時
- フィルター(selectedCategory)が更新された時
補足
- テンプレートにはカテゴリが関連付けされている
- カテゴリを削除すると関連付けされてるテンプレートも併せて削除される
export default function ItemBar() {
const { templates, categories, selectedCategory } = useContext(TemplateContext);
const [filteredTemplates, setFilteredTemplates] = useState(templates);
useEffect(() => {
if (selectedCategory === null) {
setFilteredTemplates(templates);
} else {
setFilteredTemplates(templates.filter(template => template.category_id === selectedCategory.id));
}
}, [templates, selectedCategory]);
return (
<div>
<h3>テンプレート一覧</h3>
<ul>
{filteredTemplates.map((template) => {
const category = categories.find((category) => category.id === template.category_id)
return <Item key={template.id} template={template} categoryName={category.name} />;
})}
</ul>
</div>
)
}
ですが実装してテストしてみると、テンプレートが含まれている時に限りcategory.nameのcategoryが存在しないエラーが発生しました。
原因
useState, useEffectの実行タイミングや条件を勘違いしていたことが原因でした。
useStateの方
const { templates, categories, selectedCategory } = useContext(TemplateContext);
const [filteredTemplates, setFilteredTemplates] = useState(templates);
templateは空配列で帰ってきてるから、useState(templates)
で初期化されるんじゃないか?=>されません
再レンダリング時に関してはこの初期化は適用されないのでfiltereTemplates
は更新されず前のレンダーの値が残ったままでした。
useEffectの方
useEffect(() => {
if (selectedCategory === null) {
setFilteredTemplates(templates);
} else {
setFilteredTemplates(templates.filter(template => template.category_id === selectedCategory.id));
}
}, [templates, selectedCategory]);
この部分ですが、どうもコンポーネントの描画までの流れとして
- JSXをブラウザに渡す
- useEffectの発火等の計算部分を実行
- 計算部分の値を元に描画
となるらしく、先にJSXを渡す => 古いfilteredCategory
はあるがゆえに既に削除され空となったcategoryを読み取ろうとしてエラーが発生していたようです。
// 前レンダーのfilteredTemplateが残ってるのでmapメソッドが動作
{filteredTemplates.map((template) => {
const category = categories.find((category) => category.id === template.category_id)
// categoryはContextの方で空配列になっているのでエラー発生
return <Item key={template.id} template={template} categoryName={category.name} />
}
計算のためにuseEffectを使用していますが、この使い方はReact公式でアンチパターンとして扱われています。
レンダーのためのデータ変換にエフェクトは必要ありません
useEffect は、コンポーネントを外部システムと同期させるための React フックです。
そもそもuseEffectはReact管轄外の物と同期する為に使用するフックなんですね...
対策
公式ドキュメント内に書かれていた通りstateとuseEffectを削除して関数として書き直しました。
「必要なタイミングにのみフィルター機能が動作する...」についてはuseMemoを使用しました。
const filteredTemplates = useMemo(() => {
if (selectedCategory === null) {
return templates;
} else {
return templates.filter(template => template.category_id === selectedCategory.id);
}
}, [templates, selectedCategory]);
---
{filteredTemplates.map((template) => {
const category = categories.find((category) => category.id === template.category_id)
return <Item key={template.id} template={template} categoryName={category.name} />;
})}
気づき
Reactの公式ドキュメントって凄く丁寧で分かりやすい...