コンポーネント内でコンポーネントを生成する際の問題
以下のような、コンポーネント内でコンポーネントを生成するようなコードについての話。
function App() { const Child = () => { return <div>child</div> } return ( <div className="App"> <Child /> </div> ); }
Reactは再レンダリングの際に、要素が同じtypeかを===
でチェックして、違うtypeなら、対象のコンポーネントツリーを削除して、新しく生成するらしい。
例えば、<a>
から<img>
は違うtype。<Article>
から<Comment>
も違うtype。コンポーネントの場合、参照が違うなら、違うtype。
特に気にしてなかったけど、「コンポーネント内でコンポーネントを生成する際」に問題になることがある。
以下のコードは、毎回、Childコンポーネントを新しく生成するので、typeの比較で違う参照となる。毎回コンポーネントツリーを削除して、生成していることになる。
function App() { const Child = () => { return <div>child</div> } return ( <div className="App"> <Child /> </div> ); }
なので、基本的には以下のように、コンポーネントの参照が1度だけ生成されるようにするのが良い。
const Child = () => { return <div>child</div> } function App() { return ( <div className="App"> <Child /> </div> ); }
そして、Stateを持つ場合に特に問題になる。
以下のコードでは、Appコンポーネントの中で、InnerChildコンポーネントを生成している。
const App = () => { const [count, setCount] = React.useState(0); const InnerChild = () => { const [innerCount, setInnerCount] = useState(0); return <button onClick={() => setInnerCount((s) => s + 1)}>InnerChild: {innerCount}</button>; }; return ( <div> <button onClick={() => setCount((s) => s + 1)}>{count}</button> <Child /> <InnerChild /> </div> ); }; const Child = () => { const [count, setCount] = useState(0); return <button onClick={() => setCount((s) => s + 1)}>Child: {count}</button>; };
Appコンポーネントが持つsetCount
が呼ばれるたびに、Appコンポーネントは再レンダリングされ、その子コンポーネントであるInnerChildコンポーネントやChildコンポーネントも再レンダリングされる。
この時に、InnerChildコンポーネントは、参照が変わり、typeが違うことになる。この結果、Stateがリセットされてしまう。
コンポーネントの外で定義した、Childコンポーネントは、参照が変わらないので、同じtypeと判断され、再レンダリング時もstateが保持される。カウントがリセットされることはない。
これは、Custom Hooksでコンポーネントを返すパターンを実装してると、知らず知らずのうちに、問題になることがありそう。ただ、InnerChildコンポーネントの生成にReact.useMemoを使うと、Stateが保持されたので、それで大丈夫なのかもしれない。
以下のredditで、ReactコアチームのDan Abramov
が問題についてコメントしてる。
以下の記事でも「Component Types and Reconciliation」という項目で説明している。