2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

useEffectに関する勘違い【備忘録】

Posted at

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]);

この部分ですが、どうもコンポーネントの描画までの流れとして

  1. JSXをブラウザに渡す
  2. useEffectの発火等の計算部分を実行
  3. 計算部分の値を元に描画

となるらしく、先に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の公式ドキュメントって凄く丁寧で分かりやすい...

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?