blog

Reactフックのオリジン

Reactの公式ドキュメント「Hook入門」の記事に動機の紹介がありますが、以下の内容は、読者の理解を容易にするために、より詳細な追加を行うための例の形で、このコンテンツに基づいています。 カスタムH...

Feb 12, 2020 · 7 min. read
シェア

コンポーネント間で状態ロジックを再利用するのは困難

ここで特に注意していただきたいのは、カスタムフックはステート値ではなく、ステートロジックを多重化するということです。どういう意味でしょうか?ちょっと回りくどい言い方なので、例を見て分かりやすくしましょう。サンドボックスのアドレス:codesandbox.io/s/zealous-m...

import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; function Parent() { return (<div className="parent"> <Increase /> <Decrease /> </div>) } function useCountState(type) { const [count, useCount] = useState(0); useEffect(() => { const time = (type === 'increase') ? 0 : 3000; setTimeout(() => { useCount(count + 1); }, time); }, []); return count; } function Increase() { const count = useCountState('increase'); return <p className="increase"> <span className="text">Increase {count}</span> </p>; } function Decrease() { const count = useCountState(); return <p className="decrease"> <span className="text">Decrease {count}</span> </p>; } ReactDOM.render(<Parent />, mountNode);

上の例のコンポーネント Increase と Decrease は、カスタムフック useCountState を再利用しています。その直後、'Increase result'は非常に早く1に変化し、'Decrease result'は3秒後に1に変化することがまずわかります。これは、ここでの2つのステート値は共有されておらず、このロジックだけが共有されていることを意味します。カスタムフックは他の関数と同じで、特別なものではありません。

複雑な部品は理解しにくい

1つは、問題のロジックは、componentDidMount、componentDidUpdate、componentWillUnmontといった異なるライフサイクルで記述する必要があるため、断片化されていることです。2つ目は、状態を管理するためにreducersを使うことです。状態管理ライブラリを使う主な理由は、コンポーネントを横断して状態を共有するためであって、リデューサで状態を管理する方が便利だからではないと思います。

最初の点については、useEffectを使うことで利便性が向上し、ロジックがより理にかなったものになります。以下は、メニューを収集するために空白の領域をクリックする一般的な例です:サンドボックスのアドレス:codesandbox.io/s/distracte...

import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; function DownDropMenus() { const [show, setShow] = useState(false); const handleClick = () => { setShow(!show); } const handleClickSpace = (e) => { if (e.target !== this.dom) { setShow(false); } } useEffect(() => { document.addEventListener('click', handleClickSpace); return () => { document.removeEventListener('click', handleClickSpace); } }, [show]); const display = (show) ? 'block' : 'none'; return <div className="down-drop-menus"> <span className="text" onClick={handleClick} ref={(dom) => { this.dom = dom; }}>ドロップダウンメニュー</span> <ul className="menus" style={{ display }}> <li>メニュー項目I</li> <li>メニュー項目2</li> <li>メニュー項目3</li> </ul> </div> } ReactDOM.render(<DownDropMenus />, mountNode);

対応するCSS

.down-drop-menus {
 position: relative;
 .text {
 cursor: pointer;
 }
 .menus {
 position: absolute;
 top: 24px;
 list-style: none;
 margin: 0;
 padding: 8px 0;
 border: 1px solid #ddd;
 
 li {
 line-height: 28px;
 padding: 0 16px;
 
 &:hover {
 background-color: #f5f5f5;
 cursor: pointer;
 }
 }
 }
}

難しいクラス

この記事の冒頭で「Reactを学ぶにはクラスが大きな障壁になる」という一節がありますが、その障壁が何なのかはわかりません。ただ、この段落から拾ったポイントは2つあります。ひとつはthisを理解することで、Reactのクラスオブジェクトのメソッドthisは、コンポーネントそのものを指すようにバインドしたり、特定の書き方をする必要があること、もうひとつは、関数コンポーネントとクラスコンポーネントの適用シナリオを区別することです。フックは、関数コンポーネントを使うのが好きな人たちにとって恩恵となるはずです。フックは、クラス・コンポーネントを必要とするほとんどすべてのシナリオに適応することができます。

さらに、公式サイトでは、Prepackを使用してコンポーネントの折りたたみについて実験したところ、クラスコンポーネントを使用することで、最適化が効果的に行われないソリューションを使用することを開発者に不注意に促すことがわかったと述べています。クラスはコンストラクタを持たなければならないため、クラスコンポーネントを使用した同じ機能を持つコンポーネントは、関数コンポーネントよりも多くのコードでコンパイルされる可能性が高くなります。

アプリケーションの範囲

私見ですが、クラスコンポーネントも、フックに対応したファンクションコンポーネントも、クラスの概念や機能を省き、クラスよりも書きやすいという点では大差ないと思います。Hookはclassよりも書きやすく、classの概念や特徴を省いています。 また、functionの特徴として、あまり大きなコンポーネントを管理するのは良くないので、ついつい小さなコンポーネントに分割するように無理をしてしまいます。以上の3つの項目と本節の内容を合わせると、公式サイトではHookを使った実装を推奨・推奨しており、また、ほとんどのシナリオに当てはまります。現在のところ、あまり一般的ではないgetSnapshotBeforeUpdate、getDerivedStateFromError、componentDidCatchのライフサイクルに相当するHookはありません。実際、getDerivedStateFromPropsも現在のHookではサポートされていませんが、このメソッドはcomponentDidMount / componentDidUpdateで機能的に置き換えることができるので、機能の実装には影響しません。しかし、propsのプロパティがstateのプロパティと一致しなければならないとき、もう一回実行される必要があるという影響があります。例を参照してください: サンドボックスのアドレス:codesandbox.io/s/staging-b...

import React, { useState, useEffect, memo } from 'react'; import ReactDOM from 'react-dom'; import { Button, Modal } from 'antd'; function TestDialog({ onVisibleChange, visible: pVisible }) { const [visible, setVisible] = useState(false); // 2回実行された console.log(pVisible, visible); const handleHide = () => { setVisible(false); if (typeof onVisibleChange === 'function') { onVisibleChange(false); } } useEffect(() => { setVisible(pVisible); }, [pVisible]); return <span> <Modal visible={visible} title="test" onClose={handleHide} onCancel={handleHide} onOk={handleHide}>Test</Modal> </span> } function Parent() { const [visible, setVisible] = useState(false); const handleClick = () => { setVisible(true); } const handleVisibleChange = (visible) => { setVisible(visible); } return (<div> <Button type="primary" onClick={handleClick}>Open</Button> <TestDialog visible={visible} onVisibleChange={handleVisibleChange} /> </div>) } ReactDOM.render(<Parent />, mountNode);

開く」ボタンをクリックしたり、ダイアログボックスを「閉じる」たびに、8行目のコンソールが2回出力されます。

クラスコンポーネントでgetDerivedStateFromPropsを使用すれば、上記の問題は回避できます。コードに示されているように:サンドボックスのアドレス:

import React, { Component, useState } from 'react'; import ReactDOM from 'react-dom'; import { Button, Modal } from 'antd'; class TestDialog extends Component { state = { visible: false } static getDerivedStateFromProps(props, state) { if (props.visible !== undefined && props.visible !== state.visible) { return { visible: props.visible }; } return null; } handleHide = () => { const { onVisibleChange } = this.props; this.setState({ visible: false }); if (typeof onVisibleChange === 'function') { onVisibleChange(false); } } render() { const { visible } = this.state; // 一度だけ実行する console.log(visible, this.props.visible); return (<span> <Modal visible={visible} title="test" onClose={this.handleHide} onCancel={this.handleHide} onOk={this.handleHide}>Test</Modal> </span>) } } function Parent() { const [visible, setVisible] = useState(false); const handleClick = () => { setVisible(true); } const handleVisibleChange = (visible) => { setVisible(visible); } return (<div> <Button type="primary" onClick={handleClick}>Open</Button> <TestDialog visible={visible} onVisibleChange={handleVisibleChange} /> </div>) } ReactDOM.render(<Parent />, mountNode);

上記は、フックのモチベーションに関する筆者自身の理解の一部です。もし不正確な点があれば、遠慮なくご批判、ご訂正ください!

Read next

JavaSE問題018 JVMにおけるガベージコレクション

オープニングの紹介\n\nGCとは? なぜGC?\nJavaには、オブジェクトがスコープ外になったかどうかを自動的に監視し、メモリーを再要求するGC機能があります。\n割り当てられたメモリを解放する明示的な方法はありません。\nヒープで、割り当てられたメモリが解放されたことを確認します。

Feb 11, 2020 · 6 min read