Ponz Dev Log

ゆるくてマイペースな開発日記

Reactのhookを触る

ここ1~2年のお仕事フロントエンド開発でSPAといえばAngularばかりだったのですが、 最近になってReactを学び直すモチベーションが高まったのでhookを学ぶことにしました。 Reactを最後に開発で使ったのは2年以上前なので、hookについては完全に初心者です。

この投稿ではReact公式のガイド "Reactの流儀"で紹介されているミニアプリにReact hookを適用した時のメモです。 独自のhookについては扱いません。

ja.reactjs.org

そもそもhookとは?

hookは関数コンポーネントにstateやライフサイクルメソッドの機能を組み込む仕掛けと理解しました。

ReactでUIの部品を構成するコンポーネントは、 React.Componentクラスを継承したクラスコンポーネントとReact要素を返す関数である関数コンポーネントの2つに分類されます。

前者のクラスコンポーネントコンポーネントの中でユーザーのアクションや時間経過で変化する値であるstateを扱うことができます。 また、コンポーネントのマウント/アンマウントのタイミングで処理を実行できるライフサイクルメソッドを利用できます。 しかし、関数コンポーネントはstateとライフサイクルメソッドを利用できません。

hookを使うと、クラスコンポーネントでしか使えなかったstateやライフサイクルメソッドに相当する機能を関数コンポーネント組み込むことができます。

ja.reactjs.org

hookの種類

ひとまとめにhookといっても、React組み込みのhookだけでもいくつか種類があります。 React公式のドキュメントでは、"ステートフック"と"副作用フック"が紹介されています。

ステートフック

関数コンポートで状態を扱うhook。React.Componentのstateに相当する機能です。useState で使えるようになります。

副作用フック

画面表示に影響を与える副作用を持った処理を行うhook。useEffect で使えるようになります。 APIの呼び出し/クリーンアップなどクラスコンポーネントのライフサイクルメソッドでバラバラに実行していた処理を、 hook内で1箇所にまとめて宣言、実行できます。

React.Componentの代わりにhookを使ってみる

実際にここからミニアプリを題材に、クラスコンポーネントから関数コンポーネント + hookに置き換えてみます。

ステートフック

まずは検索フィールドと商品リストを表示するコンポーネントをクラスコンポーネントで書いてみます。 状態として検索フィールドの入力値をstateに持ったコンポーネントです。

クラスコンポーネントの例

import React from 'react';
import SearchBar from "./SearchBar";
import ProductTable from "./ProductTable";

class FilterableProductTable extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            filterText: '',
            isStockOnly: false,
        };

        this.handleTextChange = this.handleTextChange.bind(this);
        this.handleCheckChange = this.handleCheckChange.bind(this);
    }

    handleTextChange(e) {
        this.setState({filterText: e.target.value});
    }

    handleCheckChange(e) {
        this.setState({isStockOnly: !this.state.isStockOnly});
    }

    render() {
        return (
            <React.Fragment>
                <SearchBar
                    text={this.state.filterText}
                    onTextChange={this.handleTextChange}
                    onCheckChange={this.handleCheckChange}
                />
                <ProductTable
                    filterText={this.state.filterText}
                    isStockOnly={this.state.isStockOnly}
                />
            </React.Fragment>
        );
    }
}

export default FilterableProductTable;

次に上記のクラスコンポーネントと同等のUIを関数コンポーネント + hookで実現させます。

関数コンポーネント + hookの例

import React, {useState} from 'react';
import SearchBar from "./SearchBar";
import ProductTable from "./ProductTable";

function FilterableProductTable() {
    const [filterText, setFilterText] = useState('');
    const [isStockOnly, setIsStockOnly] = useState(false);
    const handleTextChange = event => setFilterText(event.target.value);
    const handleCheckChange = event => setIsStockOnly(!isStockOnly);

    return (
        <React.Fragment>
            <SearchBar
                text={filterText}
                onTextChange={handleTextChange}
                onCheckChange={handleCheckChange}
            />
            <ProductTable
                filterText={filterText}
                isStockOnly={isStockOnly}
            />
        </React.Fragment>
    );
}

export default FilterableProductTable;

クラスコンポーネントと比較して、関数コンポーネント + hookの方がだいぶスッキリしました。 子コンポーネントに渡すコールバック関数をthisにバインドする必要もなくなるので、これを忘れてイベントが発火しないといった事態が避けられるのも嬉しいですね。

また、setXxxで必要なプロパティだけ書き換えられるのも見通しがよいです。 管理する状態が多くなりすぎてsetXxxだらけになると必然的に見通しが悪くなるので、コンポーネントの責務が不適切と気づくきっかけにもなりそうですね。

副作用フック

続いてライフサイクルメソッドもクラスコンポーネントから関数コンポーネント + hookに置き換えてみます。 コンポーネントをマウント後に商品一覧を読み込んで表示するコンポーネントを例にします。

クラスコンポーネントの例

import React from 'react';
import FilterableProductTable from "./FilterableProductTable";

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            products: [],
        };
    }
    
    componentDidMount() {
        this.setState({products: PRODUCTS});
    }
    
    render() {
        return <FilterableProductTable products={this.state.products}/>;
    }
}

export default App;

上記のコンポーネントをhookで書き換えます。

関数コンポーネント + hookの例

import {useState, useEffect} from 'react';
import FilterableProductTable from "./FilterableProductTable";

const PRODUCTS = [...];

function App() {
    const [products, setProducts] = useState([]);

    useEffect(() => {
        // 本来は何かしらのAPI呼び出しで商品リストを取得する
        setProducts(PRODUCTS)
    }, []);

    return (
        <FilterableProductTable products={products}/>
    );
}

export default App;

正直このミニアプリだとライフサイクルメソッドが1箇所かつ単純な処理しか実行していないので嬉しさをさほど感じません。 componentDidMountcomponentWillUnmount にバラバラに処理が書かれている場合は、useEffectの関数の中に処理が纏まるので見通しが良くなりそうです。


Reactのクラスコンポーネントをhookで書き直す例でした。 既存の重厚なクラスコンポーネントを書き直すのはハードルが高いですが、 新しいコンポーネントの開発や関数コンポーネントに状態が欲しくなったときには少しずつ使っていきたいです。

以上。