ネストした子コンポーネントから親の状態を変えるReactアプリケーションで、少し試行錯誤したことを書きとめます。経緯の説明からはじめますので、タイトルの警告(Warning)の意味が知りたいという方は最後の「レンダーが済んでから親の状態を変える」をお読みください。
配列データから動的にドロップダウンメニューとコンポーネントをつくる
サンプルとしてつぎのコードで、ドロップダウンメニューと子のコンポーネントをつくります。同じ配列データ(selections
)を用いて、それぞれ動的にテンプレートを組み立てました。3つのコンポーネントはメニューで切り替わり、表示されるのはつねにひとつだけです。
import React, { useState } from 'react';
function App() {
const selections = [
{name: 'Home', component: Home},
{name: 'Products', component: Products},
{name: 'About', component: About},
];
const [selection, setSelection] = useState(selections[0].name);
return (
<div className="App">
<h1>Selection</h1>
<div>
<select onChange={(event) => setSelection(event.target.value)}>
{selections.map((selection, id) => (
<option key={id}>{selection.name}</option>
))}
</select>
<div>
{selections.map((_selection, id) => {
const Component = _selection.component;
return (
_selection.name === selection ?
<Component name={_selection.name} key={id} /> :
null
);
})}
</div>
</div>
</div>
);
}
const Home = ({ name }) => (<h2>{name}</h2>);
const Products = ({ name }) => (<h2>{name}</h2>);
const About = ({ name }) => (<h2>{name}</h2>);
export default App;
ひとつポイントとなるのは、以下に抜き書きした子コンポーネントのテンプレートをつくるコードです。配列からデータ(_selection
)を取り出して、前述のとおりメニューの選択(selection
)と一致するコンポーネントを表示しました。
このとき、コンポーネントをオブジェクトのドット参照(_selection.component
)で定めるのは適切ではありません。クラスと同じアッパーキャメルケースが推奨されているからです(「[翻訳] Airbnb React/JSX Style Guide」「命名規則」参照)。そのため一旦変数(Component
)に取り出しました。
{selections.map((_selection, id) => {
const Component = _selection.component;
return (
_selection.name === selection ?
<Component name={_selection.name} key={id} /> :
null
);
})}
これで、プルダウンメニューによりコンポーネントが切り替えられるようになりました(図001)。
図001■プルダウンメニューでコンポーネントが切り替わる
コンポーネントのテキストカラーを変える
プルダウンメニューでコンポーネントを切り替えたとき、ルートコンポーネント(App
)のテキスト(<h1>
要素)の色を変えます。この簡単なサンプルでは、メニューを選んだイベント(onChange
)でそのカラー値を変数(color
)にとり、テキストカラーに反映させればよいでしょう。
function App() {
const selections = [
{name: 'Home', component: Home, color: 'blue'},
{name: 'Products', component: Products, color: 'green'},
{name: 'About', component: About, color: 'red'},
];
const [color, setColor] = useState(selections[0].color);
return (
<div className="App">
{/* <h1>Selection</h1> */}
<h1 style={{color: color}}>Selection</h1>
<div>
{/* <select onChange={(event) => setSelection(event.target.value)}> */}
<select onChange={(event) => {
const name = event.target.value;
setSelection(name);
setColor(selections.find((_selection) => _selection.name === name).color);
}}>
</select>
</div>
</div>
);
}
これでメニューを選ぶと、コンポーネントだけでなく、テキストの色も変わります(図002)。
図002■メニューを選ぶとテキストの色が変わる
子コンポーネントから親の状態を変える
子コンポーネントの切り替えが、メニュー以外からも行われるとしたらどうでしょう。たとえば、子コンポーネントの中に他のコンポーネントに移動するボタンがあるような場合です。それでも、ボタンクリックしたときの切り替え処理そのものは、親が一手に握るという対応が考えられます(多くの場合そうでしょう)。
ところが、本稿を書くきっかけとなったアプリケーションでは、子コンポーネントがルーターで遷移しました。つまり、ブラウザのナビゲーション(たとえば戻るボタン)でも、コンポーネントが切り替わってしまうのです。そこで、子コンポーネントの側から、自分が表示されたことを親に伝えようと考えました。
つぎのように、子コンポーネントにそれぞれのカラー値と親の状態設定関数(setColor
)を渡し、子の関数本体で設定をさせます。これで一応、コンポーネントの切り替えに応じて、親のテキストカラーが変わるようにはなりました。
function App() {
return (
<div className="App">
<div>
{/* <select onChange={(event) => {
}}> */}
<select onChange={(event) => setSelection(event.target.value)}>
{selections.map((selection, id) => (
<option key={id}>{selection.name}</option>
))}
</select>
<div>
{selections.map((_selection, id) => {
const Component = _selection.component;
return (
_selection.name === selection ?
// <Component name={_selection.name} key={id} /> :
<Component
color={_selection.color}
setColor={setColor}
/> :
null
);
})}
</div>
</div>
</div>
);
}
// const Home = ({ name }) => (<h2>{name}</h2>);
const Home = ({ name, color, setColor }) => {
setColor(color);
return (<h2>{name}</h2>)
};
// const Products = ({ name }) => (<h2>{name}</h2>);
const Products = ({ name, color, setColor }) => {
setColor(color);
return (<h2>{name}</h2>)
};
// const About = ({ name }) => (<h2>{name}</h2>);
const About = ({ name, color, setColor }) => {
setColor(color);
return (<h2>{name}</h2>)
};
レンダーが済んでから親の状態を変える
メニューの切り替えを試してみると、ブラウザコンソールにはたとえばつぎのような警告が示されます。どうも、子コンポーネントから親の状態設定関数を呼び出すことが叱られているようです。
Warning: Cannot update a component (
App
) while rendering a different component (Products
). To locate the bad setState() call insideProducts
, follow the stack trace as described in https://fb.me/setstate-in-render
Reactコンポーネントは、他のコンポーネントがレンダー中は副作用、つまりその状態を変えることが許されないのです。「React v16.13.0」の「Warnings for some updates during render」にはつぎのように説明されています(訳: 筆者)。
It is supported to call
setState
during render, but only for the same component. If you callsetState
during a render on a different component, you will now see a warning:
レンダー中に状態設定関数(
setState
)を呼び出すことがサポートされるのは、そのコンポーネント自身へのものにかぎられます。他のコンポーネントの状態設定関数を、そのコンポーネントがレンダーされているときに呼び出せば、つぎのような警告が示されるでしょう。
Warning: Cannot update a component from inside the function body of a different component.
ではどうするかというと、レンダーが済んでから状態設定関数を呼び出せばよいのです。解説はつぎのように続きます。
This warning will help you find application bugs caused by unintentional state changes. In the rare case that you intentionally want to change the state of another component as a result of rendering, you can wrap the
setState
call intouseEffect
.
**この警告により、意図しない状態変更から生じるアプリケーションのバグが見つけやすくなります。**あえて他のコンポーネントの状態を変えて、そのレンダー結果に反映したいというまれな場合は、状態設定関数の呼び出しを
useEffect
でラップしてください。
あえて親の状態変更をレンダーに反映させたい今回の「まれな場合」は、つぎのように子コンポーネントからuseEffect()
の副作用関数(第1引数)で親の状態を変えればよいということです。
// import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
const Home = ({ name, color, setColor }) => {
// setColor(color);
useEffect(
() => setColor(color),
[color, setColor]
);
return (<h2>{name}</h2>)
};
const Products = ({ name, color, setColor }) => {
// setColor(color);
useEffect(
() => setColor(color),
[color, setColor]
);
return (<h2>{name}</h2>)
};
const About = ({ name, color, setColor }) => {
// setColor(color);
useEffect(
() => setColor(color),
[color, setColor]
);
return (<h2>{name}</h2>)
};
モジュールreact:src/App.js
の記述全体を、つぎのコード001にまとめます。併せて、実際のサンプルReactアプリケーションをCodeSandboxに公開しました。
コード001■子コンポーネントから親の状態を変える
import React, { useState, useEffect } from 'react';
function App() {
const selections = [
{name: 'Home', component: Home, color: 'blue'},
{name: 'Products', component: Products, color: 'green'},
{name: 'About', component: About, color: 'red'},
];
const [selection, setSelection] = useState(selections[0].name);
const [color, setColor] = useState(selections[0].color);
return (
<div className="App">
<h1 style={{color: color}}>Selection</h1>
<div>
<select onChange={(event) => setSelection(event.target.value)}>
{selections.map((selection, id) => (
<option key={id}>{selection.name}</option>
))}
</select>
<div>
{selections.map((_selection, id) => {
const Component = _selection.component;
return (
_selection.name === selection ?
<Component
name={_selection.name}
key={id}
color={_selection.color}
setColor={setColor}
/> :
null
);
})}
</div>
</div>
</div>
);
}
const Home = ({ name, color, setColor }) =>{
useEffect(
() => setColor(color),
[color, setColor]
);
return (<h2>{name}</h2>)
};
const Products = ({ name, color, setColor }) =>{
useEffect(
() => setColor(color),
[color, setColor]
);
return (<h2>{name}</h2>)
};
const About = ({ name, color, setColor }) =>{
useEffect(
() => setColor(color),
[color, setColor]
);
return (<h2>{name}</h2>)
};
export default App;