動的コンポーネントを安全、素早く作る
動的にコンポーネントを生成し、それらを意図通りに状態変化させる。
その際の設計、製造のポイント等を初級者向けに紹介。
やりたいこと
以下のすべての機能を満たすコンポーネントを作る。
-
同一コンポーネント(値違い可)の生成
テキストボックスを編集する
↓
編集に応じて、自動的に新しいテキストボックスが下に出てくる
↓
テキストボックスを編集する
…以下ループを特定条件まで実行できる -
生成したコンポーネントの一律制御
ボタンを押下することで、上記のテキストボックスをすべて非活性にする。
実践
まず1つ分のコンポーネントを生成
const [inputData, setInputData] = useState<string>("");
const [disBtnDisable, setDisBtnDisable] = useState<boolean>(false);
const [actBtnDisable, setActBtnDisable] = useState<boolean>(false);
const handleTextChange = (value: string) => {
setInputData(value);
}
const handleDisableBtn = () => {
// disable all components.
}
const handleActivateBtn = () => {
// activate all components.
}
return (<>
<Grid>
<Button onClick={handleDisableBtn} disabled={disBtnDisable}>
非活性
</Button>
<Button onClick={handleActivateBtn} disabled={actBtnDisable}>
活性
</Button>
</Grid>
<Grid>
<TextField
id="standard-basic"
label="Standard"
variant="standard"
helperText="a helper text is here."
onChange={(e) => handleTextChange(e.target.value)}
value={inputData}
disabled={false}
/>
</Grid>
</>);
このテキストフィールドコンポーネントを今回の再生成対象とする。
型定義
以下の型定義が、オブジェクト指向でいう"メンバ変数(プライベート変数)"となる。
1つ分のコンポーネントのパラメータをすべて型定義する
export type TextFieldParams = {
id:string;
label:string;
variant:string;
helpertext:string;
value:string;
disabled:boolean;
}
今回は1つのコンポーネントだけを生成するが、同時に複数コンポーネントを作成し、1度にセットで生成することも可能。
コンポーネント(群)をまとめ、IDを採番するコンポーネントを定義する(重要)
export type DinamicOjbect = {
key:number; // 以下に説明するユニークIDの生成法によりstringとなりえる
compParam1:TextFieldParams; //
}
詳細は以下に記載するが、動的コンポーネントを生成する際、Reactは生成対象ごとの親コンポーネントを「キー」という属性で識別をしている。そのため、生成対象の親に持たせる一意なキーデータを生成する必要がある。
キーを正確に設定していないと、編集内容の反映、表示などの状態の受け渡しに失敗する。
型を利用した変数、およびコンポーネントの実装
先述したコンポーネント群をまとめたものを格納する変数を定義する。
keyにはユニークIDを設定する。(重要)
変数定義
パターン1:数字型ユニークID
//初期値の生成
const initTextFieldParams:TextFieldParams = {
id: '1',
label: 'label1',
variant: 'standard',
helpertext: 'helper1',
value: '',
disabled: false
}
//中略
// ユニークIDを生成する関数
const [currentId, setCurrentId] = useState<number>(0);
function generateUniqueId() {
setCurrentId(currentId+1);
// レンダリング後でないと演算結果は反映されないため、同じ値を戻す。
return currentId+1;
}
const initDinamicObject: DinamicOjbect
= {key:currentId , compParam1:initTextFieldParams};
const [dinamicTextFieldparams, setDinamicTextFieldparams]
= useState<DinamicOjbect[]>([initDinamicObject]);
パターン2:UUIDの生成(npm install uuid)
import { v4 as uuid } from 'uuid';
// 中略
const initDinamicObject: DinamicOjbect
= {key:uuid() , compParam1:initTextFieldParams};
const [dinamicTextFieldparams, setDinamicTextFieldparams]
= useState<DinamicOjbect[]>([initDinamicObject]);
コンポーネント定義
mapを使い、dinamicTextFieldparams変数の中身を展開し、コンポーネントのパラメータに配置する。
return (<>
<Grid>
<Button onClick={handleDisableBtn} disabled={disBtnDisable}>非活性</Button>
<Button onClick={handleActivateBtn} disabled={actBtnDisable}>活性</Button>
</Grid>
<Grid>
{
dinamicTextFieldparams.map(row => (
// ここで上記のユニークIDを定義する。動的コンポーネントの親に1つだけ定義。
<Grid key={row.key}>
<TextField
id={row.compParam1.id}
label={row.compParam1.label}
variant={row.compParam1.variant}
helperText={row.compParam1.helpertext}
// 発火する関数には、ユニークIDを引数に設定しておく(重要)
onChange={(e) => handleTextChange(row.key, e.target.value)}
value={row.compParam1.value}
disabled={row.compParam1.disabled}
/>
{/** ほかに動的生成するコンポーネントがあれば同じ要領で実装 */}
</Grid>
)
)
}
</Grid>
</>);
この時点でだいぶコードがすっきりしていることがわかる。
実際、タグ記述はこれ以上行うことはない。
以降はすべて「パラメータの状態遷移」に関する記載なので、発火先の関数で操作を行う。
関数定義
ここから先が、いわゆる"メンバメソッド"の実装となる。
テキストボックスの変更があったとき発火する関数定義
const handleTextChange = (id:number, value: string) => {
// 現在パラメータの更新※注意:このままレンダリングしても値は反映されない。(オブジェクト自体変わらないため)
dinamicTextFieldparams.forEach(row => {
if (row.key === id){
row.compParam1.value = value
// 今入力したコンポーネントは値を表示し、非活性にする。
row.compParam1.disabled = true
}
})
// 新しいテキストフィールドのパラメータを作成。前回の値(発火元のコンポーネントの入力値)を利用できる。
const newId = generateUniqueId();
const newTextField:TextFieldParams = {
id: 'newTextId'+String(newId),
label: value.slice(0,3),
variant: 'standard',
helpertext: '',
value: "prev value is"+value,
disabled:false
}
// コンポーネント群をまとめ、ID採番をおこなう。
const newComps:DinamicOjbect = {
key: newId,
compParam1:newTextField
}
// レンダリング(オブジェクトを再生成することで上記の編集内容をすべて反映したコンポーネントが再生成される。)
setDinamicTextFieldparams(prev => [...prev, newComps]);
上記コメントに注意を払いつつ実装いただきたい。
特にデータの変更と表示(レンダリング)のタイミングにおいては、useStateにて、変数を再生成する必要がある点については要注意となる。
活性、非活性ボタンの制御
const handleDisableBtn = () => {
// disable all components.
dinamicTextFieldparams.forEach(row => {
row.compParam1.disabled=true
})
// レンダリング(オブジェクトを再生成することで上記の編集内容をすべて反映したコンポーネントが再生成される。)
setDinamicTextFieldparams(prev => [...prev]);
}
const handleActivateBtn = () => {
// activate last item.
dinamicTextFieldparams[dinamicTextFieldparams.length-1].compParam1.disabled=false;
// レンダリング(オブジェクトを再生成することで上記の編集内容をすべて反映したコンポーネントが再生成される。)
setDinamicTextFieldparams(prev => [...prev]);
}
上記のように、生成したコンポーネントに対して一括、あるいは特定のコンポーネントにのみ状態を変化させることも可能。クリアボタンなどで入力値をすべてクリアさせたいなども同様の実装で実現可能。
おわりに
上記のように、オブジェクト指向の考え方(コンポーネントのパラメータをプライベート変数と見立て、それをインスタンスオブジェクト(useState)として管理しメンバメソッドで編集する)を導入することで、手癖で書くと破綻しがちな動的コンポーネント生成および制御について、安全かつ正確な実装が可能となる。
コードの役割分担も明確となるため、機能の追加や改修なども行いやすくなった。
要点は以下の通り
- 型定義を行う。型はコンポーネントの型およびそれらのIDを管理する親コンポーネントの型。
- インスタンス化(useStateで実際の値を格納)する。パラメータのみを配列として扱うことで複数の同一コンポーネントを管理しやすくなる。
(JSXも配列化できるが、正直そこまでコードの見た目上の利点を感じない。) - タグの編集には、親コンポーネントごとに必ずユニークな”key"パラメータを設定する事。
- 関数によって変数を変更した後、レンダリングする際に必ずオブジェクトの再生成を行うこと。
言ってしまえば、「全部一括でガツンと書き換えます。」という、MVCでよくあるテンプレートエンジンに似たやり方なので、もしかしたらReactの考えには沿わないかも?(上記のやり方をさらに拡大して1画面全体のパラメータを管理しようと考えたら、まさにそんな感じである。)