前提・実現したいこと・現状の問題点
少々ややこしいですが、タイトル通りのことを実装したいです。
厳密に言うと、タイトルの「動的に増えるコンポーネント内で入力した値を全て親側で取得し」の部分は実装はできているのですが、下記に示すようにレンダリングに関して問題がある、という状況です。
現状のプログラムの説明
親コンポーネント内でuseStateを使って空リスト(list)を作成する
⇩
mapで回しながら、動的に追加した子コンポーネントに、Propsとしてlistを変更する関数(setItem)を渡す
⇩
子コンポーネント内にある値でlistの要素を更新する
ここで問題となっているのが、親コンポーネント内で、mapを用いてlist内の要素を子コンポーネントに渡す処理を記述しているため、memoやuseCallbackを用いても、子コンポーネントが全て再レンダリングされてしまうことです。
この問題の対処法が全く分からず、こちらで質問するに至りました。
どなたか知恵を貸していただけるとありがたいです。
質問や不十分な点があれば追記していきますので、ご指摘お願いいたします。
追記
ある程度修正したコードを下の方に追加しています。
該当のソースコード
Parent.jsx(親コンポーネント)
JavaScript
1import { useState, useCallback } from "react"; 2import { Child } from "./Child"; 3 4export const Parent = () => { 5 console.log("Parent"); 6 const [list, setList] = useState([]); 7 8 const setItem = useCallback((idx, item) => { 9 const newList = [...list]; 10 newList[idx] = item; 11 setList(newList); 12 }, []); 13 14 const onClick = () => { 15 const newList = [ 16 ...list, 17 { 18 param1: "", // この値を子コンポーネントで埋める 19 param2: "" // この値を子コンポーネントで埋める 20 } 21 ]; 22 setList(newList); 23 }; 24 25 return ( 26 <> 27 {list.map((item, idx) => { 28 return ( 29 // Parentが再描画されるたびに、すべてのChildが再描画される 30 // listの最後に要素を追加したとしても、それ以前の要素も全て再描画の対象になる 31 <Child key={idx} item={item} setItem={(item) => setItem(idx, item)} /> 32 ); 33 })} 34 <button onClick={onClick}>追加</button> 35 </> 36 ); 37};
Child.jsx(子コンポーネント)
JavaScript
1import React, { memo } from "react"; 2 3export const Child = memo(({ item, setItem }) => { 4 const onChange1 = (e) => { 5 setItem({ ...item, param1: e.target.value }); 6 }; 7 const onChange2 = (e) => { 8 setItem({ ...item, param2: e.target.value }); 9 }; 10 11 return ( 12 <> 13 <input value={item.param1} onChange={onChange1} /> 14 <input value={item.param2} onChange={onChange2} /> 15 </> 16 ); 17}); 18
追加したところ
以下が修正後のコードです。
まだ問題点があるのですが、一応、修正した箇所とその意図を説明します。
其の一
まずは、mapで回す際のkeyについては、onClick関数内で、リストに追加する要素の初期値を作成する際に、v4を用いてオブジェクトに一意の値を付与し、それをkeyとすることで対処しました。
これは動的に要素を削除することを見越して、keyにidxを設定することを避けるという意図があります。
其の二
次に、maisumakunさんにご指摘いただいたように、setItem={(item) => setItem(idx, item)}の個所で関数を再定義してしまっていたため、こちらをsetItem={setItem}に変更し、さらにidx={idx}としてpropsとして添え字を別途子側に渡すようにしました。
子側でidxをsetItemの引数に渡すことで、修正前と同様の挙動をしてくれます。
其の三
最後にsetItem関数の変更について説明します。
setItem関数は、無駄な再レンダリングを防ぐためにuseCallbackで囲っていますが、どうやら第二引数を空リストにしていると、関数定義時に参照した値が内部に保持されるらしく、そちらを改善しました(改善しきれていなかったので、下に追加で色々書いてます)。
まず上記の意味について説明します。
最初のレンダリングにてsetItemが定義されたとき、listの値は[](空リスト)となっています。
そのため、setItem内部にある const newList = [...list]; において、listの値に[](空リスト)が保持されたままの状態になっていました。
つまり、子側でsetItemを実行したとき、実行したタイミングでその時のlistの値を参照しに行くのではなく、最初に保持していた[](空リスト)が使われてしまい、うまく機能しない状態だったということです(もしかしたら当たり前のことかも)。
個人的には、要素を追加するボタンを押したタイミングでlistが更新されれば十分だったため、新たにボタンを押したときに値が変更されるstateを追加し、それをuseCallbackの第二引数に指定することで、上記の問題を解決することができました(できてなかった)。
本来この文章を書いて終わりにしようと思っていたのですが、このuseCallbackの個所にまだ問題点がありました。結局、useCallback内の関数が更新される前に追加した要素すべてが、listの初期値であるを所持しており、連続で要素を追加するとおかしな挙動を取るっぽいです、、
そんなわけで、useCallback内の関数が実行されたタイミングでlistの値が更新できたらいいな、と考えています。
ちなみに、第二引数を[list]とした場合、入力中以外のinputタグまで再レンダリングされてしまったため、こちらは没にしました。
差分を掲載
Parent.jsx(親コンポーネント)
diff
1import { useState, useCallback } from "react"; 2import { Child } from "./Child"; 3+ import { v4 } from "uuid"; 4 5export const Parent = () => { 6 console.log("Parent"); 7 const [list, setList] = useState([]); 8+ const [count, setCount] = useState(0); 9 10 const setItem = useCallback((idx, item) => { 11 const newList = [...list]; 12 newList[idx] = item; 13 setList(newList); 14- }, []); 15+ }, [count]); 16 17 const onClick = () => { 18 const newList = [ 19 ...list, 20 { 21+ key: v4(), 22 param1: "", // この値を子コンポーネントで埋めたい 23 param2: "" // この値を子コンポーネントで埋めたい 24 } 25 ]; 26 setList(newList); 27+ setCount((prevState) => prevState + 1); 28 }; 29 30 return ( 31 <> 32 {list.map((item, idx) => { 33 return ( 34- <Child key={idx} item={item} setItem={(item) => setItem(idx, item)} /> 35+ <Child 36+ key={item.key} 37+ item={item} 38+ idx={idx} 39+ setItem={setItem} 40+ /> 41 ); 42 })} 43 <button onClick={onClick}>追加</button> 44 </> 45 ); 46}; 47
Child.jsx(子コンポーネント)
diff
1import React, { memo } from "react"; 2 3- export const Child = memo(({ item, setItem }) => { 4+ export const Child = memo(({ item, setItem, idx }) => { 5 6 // 関数の中は変更していないため省略 7 8}); 9
回答2件
あなたの回答
tips
プレビュー
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。