React の関数コンポーネントはわかりにくいというお話
React の関数コンポーネントはわかりにくいというお話
関数コンポーネントの前にクラスコンポーネントの書き方のおさらい。
class Counter extends React.Component {
constructor() {
this.state = { value: 0 };
}
increment() {
this.setState({ value: this.state.value + 1 });
}
render() {
return <div>
{{ this.state.value }}
<button onClick={() => this.increment()}>Click</button>
</div>
}
}
こうやって使います。
<Counter />
この時、この Counter コンポーネントは一度だけインスタンス化されます。ボタンをクリックしても、Counter コンポーネントのインスタンスが再度作られることはありません。
型と XML 要素をマップするタイプの GUI フレームワークは大体こんな感じです。Vue.js のコンポーネントもそうですし、WPF や MAUI などの XAML もそうです。
関数コンポーネントはどうなん?
Counter コンポーネントを関数コンポーネントで書いてみます。
const Counter = () => {
const [value, setValue] = useState(0);
const increment = () => setValue(prev => prev + 1);
return <div>
{{ value }}
<button onClic={increment}>Click</button>
</div>
};
初めてこれを書いたとき、脳がパニックに陥りました。value
は const
宣言してるので、値は変わりません。なので、常に value
の値はゼロなはずなんですよね。なんで動くんすか?
動いているものは動いているので。要は、この Counter 関数はボタンクリックするたびに呼び出されているわけです。これは一般的な GUI フレームワークの挙動と異なるのでかなり混乱します。
んでんで
関数コンポーネントの関数が毎回呼び出されると認識すればいろんなことが腑に落ちます。
useState(0)
は最初の呼び出しでは 0 を返しますが、setValue()
が呼び出さると再度 Counter 関数が呼び出されます。そうすると useState(0)
は 1 を返します。useState(0)
が 0 を返さないことがあるなんてちょっとパニックです。
また、コンポーネントが最初に表示されたときに、特定の処理を実行するときは次のように書いてはいけないわけです。
const MyComponent = () => {
// 一度しか呼び出されないつもり。
setInternval(() => console.log('hello'), 1000);
return <div>...</div>
};
このコードは問題なく動くと思うのですが、MyComponent コンポーネントがステートをもっていると話が変わります。ステートが変わるたびに MyComponent 関数は呼び出されるため、そのたびに setInterval()
が呼び出されておかしなことになります。
それを避けるために、useEffect()
という React Hook を用います。
const MyComponent = () => {
useEffect(() => setInternval(() => console.log('hello'), 1000), []);
return <div>...</div>
};
useEffect()
は初回のみ呼び出されるではなく、第二引数の値が前回と異なったら第一引数の関数を呼び出すやつです。(たぶん) 今回は []
を渡していているので、初回だけ呼び出され、二回目以降は値が変わらないので呼び出されないわけです。
ははあ。なるほど。
最後に
React の関数コンポーネントは予想と違う動きをします。そしてそれを知る必要があります。こういう仕組みはあまりよくないと思ってます。お前の技術力不足やろと言われたらその通りなのですが...
クラスコンポーネントの方が分かりやすいのでそっちで書きたいんですけどねぇ。React Hook を呼び出せないという重大な欠点が...
Discussion