質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Next.js

Next.jsは、Reactを用いたサーバサイドレンダリングなどを行う軽量なフレームワークです。Zeit社が開発しており、nextコマンドでプロジェクトを作成することにより、開発環境整備が整った環境が即時に作成できます。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

React.js

Reactは、アプリケーションのインターフェースを構築するためのオープンソースJavaScriptライブラリです。

Q&A

解決済

2回答

1702閲覧

動的に増えるコンポーネント内で入力した値を全て親側で取得し、かつ無駄な再レンダリングをなくしたい

tksx1227

総合スコア6

Next.js

Next.jsは、Reactを用いたサーバサイドレンダリングなどを行う軽量なフレームワークです。Zeit社が開発しており、nextコマンドでプロジェクトを作成することにより、開発環境整備が整った環境が即時に作成できます。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

React.js

Reactは、アプリケーションのインターフェースを構築するためのオープンソースJavaScriptライブラリです。

0グッド

0クリップ

投稿2021/07/06 05:33

編集2021/07/06 12:46

前提・実現したいこと・現状の問題点

少々ややこしいですが、タイトル通りのことを実装したいです。

厳密に言うと、タイトルの「動的に増えるコンポーネント内で入力した値を全て親側で取得し」の部分は実装はできているのですが、下記に示すようにレンダリングに関して問題がある、という状況です。

現状のプログラムの説明
親コンポーネント内で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

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

自己解決

useReducerを用いてstateを管理するように変更することで、期待していた挙動をするようになりました。

具体的に改善できたこととしては、入力中のinputタグのみ再レンダリングを走らせて、それ以外のinputタグは自身に変更が加わるまで再レンダリングされない、という感じです。

初期のコードをがらっと書き換えたため、差分ではなくソースコードをべた張りしておきます。

maisumakunさん、最終的には自己解決に近い形になってしまいましたが、長々とお付き合いいただきありがとうございました。

修正後のソースコード

Parent.jsx

JavaScript

1import { useCallback, useReducer } from "react"; 2import { v4 } from "uuid"; 3 4import { Child } from "./Child"; 5 6const initState = []; 7export const ACTION_TYPE = { 8 ADD: "add", 9 UPDATE: "update" 10}; 11 12const reducer = (state, action) => { 13 switch (action.type) { 14 case ACTION_TYPE.ADD: 15 return [...state, action.payload]; 16 case ACTION_TYPE.UPDATE: { 17 const newState = [...state]; 18 newState[action.idx] = action.payload; 19 return newState; 20 } 21 default: 22 return state; 23 } 24}; 25 26export const App = () => { 27 const [state, dispatch] = useReducer(reducer, initState); 28 29 const onClick = useCallback(() => { 30 const newItem = { 31 key: v4(), 32 param1: "", 33 param2: "" 34 }; 35 dispatch({ type: ACTION_TYPE.ADD, payload: newItem }); 36 }, []); 37 38 const onClickLog = useCallback(() => { 39 console.log(state); 40 }, [state]); 41 42 return ( 43 <> 44 <button onClick={onClick}>追加</button> 45 <button onClick={onClickLog}>log</button> 46 {state.map((item, idx) => { 47 return ( 48 <Child key={item.key} item={item} idx={idx} dispatch={dispatch} /> 49 ); 50 })} 51 </> 52 ); 53}; 54

Child.jsx

JavaScript

1import React, { memo } from "react"; 2 3import { ACTION_TYPE } from "./App"; 4 5export const Child = memo(({ item, dispatch, idx }) => { 6 console.log(`${idx} Child`); 7 const onChange1 = (e) => { 8 const payload = { ...item }; 9 payload.param1 = e.target.value; 10 dispatch({ type: ACTION_TYPE.UPDATE, payload, idx }); 11 }; 12 const onChange2 = (e) => { 13 const payload = { ...item }; 14 payload.param2 = e.target.value; 15 dispatch({ type: ACTION_TYPE.UPDATE, payload, idx }); 16 }; 17 18 return ( 19 <> 20 <input value={item.param1} onChange={onChange1} /> 21 <input value={item.param2} onChange={onChange2} /> 22 </> 23 ); 24}); 25

投稿2021/07/07 08:47

tksx1227

総合スコア6

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

<Child key={v4()}としていますが、このように毎回違うkeyを与えるということは、強制的にコンポーネントの再構築を行う指示をしているのに等しい行為です。

投稿2021/07/06 05:38

maisumakun

総合スコア146018

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

tksx1227

2021/07/06 05:46 編集

回答ありがとうございます。 確かにフォーカスが外れるという問題は、key={v4()}と変更してから生じたものでした... しかし、元々key={idx}としていたのですが、この場合でも無駄な再レンダリングが生じていました。 こちらの改善方法はあるのでしょうか。 (ややこしくなるのでkey={v4()}の部分は訂正させていただきます。)
maisumakun

2021/07/06 05:49

setItem={(item) => setItem(idx, item)}と書いている以上、この関数はレンダリングのたびに違う関数となって、イベントハンドラの差し替えが必要となります。 (描画ごとに変化する値ではない)setItemとidxを別個に渡して子コンポーネントの内部で処理させる、あるいは(item) => setItem(idx, item)という関数をキャッシュして次の描画にも再利用する、といった工夫が必要です。
tksx1227

2021/07/06 05:55 編集

(item) => setItem(idx, item)という関数をuseCallbackで囲むことができないため、この関数が再生成されている、という認識でよろしいでしょうか。 親コンポーネント内で、setItemをuseCallbackで囲っているので大丈夫だと思っていたのですが、上の関数は、setItemとは別の関数だから関係ないということで合っていますか。
maisumakun

2021/07/06 06:02

どちらも正しいです。setItemがuseMemoされていようが、(item) => setItem(idx, item)は生成のたびに別なインスタンスとなります。
tksx1227

2021/07/06 06:13

なるほど、大分問題点が明確になった気がします。 とりあえず上記を参考にしてコードを修正してみますね。
maisumakun

2021/07/06 13:13

> ちなみに、第二引数を[list]とした場合、入力中以外のinputタグまで再レンダリングされてしまったため、こちらは没にしました。 その程度の更新は「気にしない」という選択肢もありです。
tksx1227

2021/07/06 13:27

「気にしない」という選択肢は考えていなかったですね、、 実際に作っているプログラムでは、追加ボタンを押すたびに4つで1セットの入力欄が増えるのですが、さほど気にしなくても差し支えないということでしょうか。 実際、入力のたびにすべての子コンポーネントが再レンダリングされると、最初は4つ、1つ追加すると8つ、さらにもう1つ追加すると12、、、と1文字入力するごとに4の倍数個分のinputタグが再レンダリングされることになると思うのですが、これだと厳しいですかね。 当方、Reactを学び始めてまだ日が浅いということもあり、いまいち最適化の加減が分かっておらず、長々と思考を巡らせてしまっています、、
maisumakun

2021/07/06 13:31

> 1文字入力するごとに4の倍数個分のinputタグが再レンダリングされることになると思うのですが、これだと厳しいですかね。 よっぽどリアルタイム性が求められる、あるいはもっと多くない限り、じゅうぶん実用的な性能は出ます。
maisumakun

2021/07/06 13:33

ゲームなどリアルタイム性が肝になるようなプロダクトでないなら、最適化は速度が問題になってから考える、でもとりあえずは間に合います。
tksx1227

2021/07/06 13:39

>よっぽどリアルタイム性が求められる、あるいはもっと多くない限り、じゅうぶん実用的な性能は出ます。 上記のセットは最大で100個まで追加できるようにする予定です。 つまり、一文字入力するごとに最大400個のinputタグが再レンダリングされるということになります。 この場合は”もっと多くない限り”に該当するのでしょうか。
maisumakun

2021/07/07 00:27

気になるなら、実際に実装してどの程度の性能が得られるか確認してみましょう。
tksx1227

2021/07/07 07:29

50個近く生成した段階で、かなり重くなってしまい使い物にならなさそうです、、
maisumakun

2021/07/07 07:34

ブラウザにReact Developer Toolsを入れてみて、どこがどれだけ更新されていて、それがどれだけの時間がかかっているのか検証してみましょう。他の箇所・値が原因ということも考えられます。
maisumakun

2021/07/07 07:36 編集

「既存のstateをもとにして新たなstateを作る」ような操作を行う場合、setStateに「前のstateを受け取って新しいstateを返す関数」を渡す形とすれば、前のstateでメモ化する必要がなくなることがあります。
tksx1227

2021/07/07 08:00

useReducerでstateを管理する方向に切り替えるとうまくいきそうなので、こちらを先に試してみます。
tksx1227

2021/07/07 08:51 編集

結果的にuseReducerでうまくいったので、一応、自己解決として処理しておきました。 上の方でも書かせていただきましたが、maisumakunさん、長々とお付き合いいただきありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問