ZEALS TECH BLOG

チャットボットでネットにおもてなし革命を起こす、チャットコマース『Zeals』を開発する株式会社ZEALSのテックブログです。技術やエンジニア文化について情報を発信します。

React Hooksを使用してみました!

f:id:zeals-engineer:20190809134906p:plain (※画像はReact Hooks Tutorialから引用)

はじめに

こんにちは!
普段RailsとReactを書いている中村です!

最近本格的にReactを書きはじめたのですが、今年リリースされたhooksを早速触ってみました!
今回はhooksを触って得た知見を共有していこうと思います!

hooksとは

一言で説明すると
state管理やライフサイクルメソッド等がFunctional Componentで使用できる
という感じです!

百聞は一見に如かずなので、早速コードと共に説明していきます!
※HooksはReact 16.8より古いversionだと動きません。

useState

Functional Componentにstateを持たせられる APIです。

useStateの基本的な構文

const [state, setState] = useState(initialState);

stateの遅延初期化

initialStateは初回render時に使われるstateの値です。後続のrender時にはその値を使いません。
もし初期のstateに複雑な処理が必要な値である場合は、代わりに関数を渡すことができます。この関数は初回のrender時にのみ実行されます。

const [state, setState] = useState(() => {
  const initialState = somethingFunction(props);
  return initialState;
});

useStateはステートフルな値と、それを更新するための関数を返します。
それらを分割代入で取り出し、変数として定義しています!

基本的な構文を説明したところで早速実践に移っていきます!
ボタンを押すと数が増えたり減ったりする簡易的なAppを例にしてみていきましょう。
hooksが導入される以前は状態管理をするために、Class Componentを使う以外に方法はありませんでした

こんな感じ
↓↓↓

import React, { Component } from "react";

class Counter extends Component {
  state = { count: 0 };
  
  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button
          onClick={() =>
            this.setState(prevState => {
              return { count: prevState.count + 1 };
            })
          }
        >
          +1
        </button>
        <button
          onClick={() =>
            this.setState(prevState => {
              return { count: prevState.count - 1 };
            })
          }
        >
          -1
        </button>
      </div>
    );
  }
}

hooksを導入することにより、Functional Componentでも状態管理ができるようになりました!
こんな感じです
↓↓↓

import React, { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(0)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+1</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>-1</button>
    </div>
  );
};

どうでしょうか?Hooksで書いた方がシンプルでわかりやすいと思います。
Class Componentではstateがオブジェクトである事、this.setStateがオブジェクトの一部を更新する関数のため冗長に感じます(個人的に)。
Hooksは、countというstateに対してsetCountという専用の関数を使用しているので、より直感的なコードになっていると思います!

useEffect

レンダリング後に処理を行うAPIです。 useEffectはClass Componentのライフサイクルメソッドでいえば、
componentDidMountcomponentDidUpdateに相当するものです(厳密には少し違う) 早速みていきましょう!

const Counter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("like componentDidMount or ComponentDidUpdate");
  }); //①第二引数省略

  useEffect(() => {
    console.log("like componentDidMount");
  }, []); //②第二引数に空配列

  useEffect(() => {
    console.log("only change count");
  }, [count]); //③第二引数にcountを渡す


  const increment = () => setCount(prevCount => prevCount + 1);
  const decrement = () => setCount(prevCount => prevCount - 1);

  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
    </div>
  );
};

第1引数にはコールバック関数、第2引数には値の配列を定義し、配列のいずれかの値が変わったときのみ、コールバック関数を実行します。

  1. は上記でも記載した通り、componentDidMountcomponentDidUpdateのタイミングでコールバック関数が実行されます。
  2. はcomponentDidMountのタイミングのみコールバック関数を実行します。(配列に値が存在しないため)
  3. はcountの値に変更があった時のみコールバック関数を実行します。

今回の場合はstateがcountだけなので、第2引数を省略しても変わりませんが、他にもstateがある場合に不必要な処理が発生しないようにする効果があります。
以前のclass ComponentではcomponentDidMountやcomponentDidUpdateに同じ処理を書くことがあったので、useEffectで一括に管理ができ、コードがより綺麗になります。

useContext

React.createContext からの戻り値を受け取り、そのContextの現在値を返します。
contextとはReact V16.3で導入されたAPIです。

Contextはprop drilling問題を解決します。
最初にuseContextを使わずにContextのみを使った実装だと こんな感じです 。
↓↓↓

import React, { createContext } from "react";
const RootContext =createContext();

const GrandChild = () => (
  <RootContext.Consumer>
    {value => <p>{value.name}</p>}
  </RootContext.Consumer>
);

const Child = () => <GrandChild />;

const Father = () => {
  return (
    <RootContext.Provider value={{ name: "taro" }}>
      <Child />
    </RootContext.Provider>
  );
};

本来なら、Child Componentからpropsとして{name: 'taro'}をGrandChild Componentに渡してあげる必要があります!
Contextのみでもprops drilling問題は解決できますが useContextを使った方がより簡潔でわかりやすいです!

useContextを使うとこんな感じです。
↓↓↓

import React, { createContext, useContext } from "react";
const Context = createContext();

const GrandChild = () => {
  //分割代入
  const { name } = useContext(Context);
  return <p>{name}</p>;
};

const Child = () => <GrandChild />;

const Father = () => {
  return (
    <Context.Provider value={{ name: "taro" }}>
      <Child />
    </Context.Provider>
  );
};

useContextの引数に作成したContextを渡すとvalueの値が返ってきます(今回でいうと{name: 'taro'})

いかがでしょうか?
Contextのみを使用するより、useContextとContextを合わせて使用した方が、簡潔で読みやすいですよね?
僕個人prop drilling問題にかなり悩まされていたので、もっと早く知りたかったです。。。笑

useReducer

useReducerはuseStateの代替品です。
基礎構文

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducerは、現在のstateをdispatchメソッドとペアにして返します。
useStateとの使い分けとして、複数の値にまたがる複雑なstateロジックがある場合はuseReducerを使う方が好ましいと思われます。

また、useReducerを使えばコールバックの代わりにdispatchを下位コンポーネントに渡せるようになるため、複数階層にまたがってstateの更新を 発生させるようなコンポーネントでは効果的であると思います。
コードは以下のようになります!

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

初期stateの指定 useReducerの初期化の方法には 2 種類あります。最も単純な方法は第 2 引数として初期stateを渡すものです。

 const initialState = {count: 0}
 const [state, dispatch] = useReducer(reducer, initialState);

注意

React では、初期値はpropsに依存している可能性があるため、フックの呼び出し部分で指定します。
そのため、**reducerの引数でstate = initialState のように、Reduxで普及した慣習を推奨しておりません。**  

普段Reduxを使っている方は若干違和感を覚えるかもしれません!
Reactの公式ドキュメントに記載されているので、気になる方はこちらからどうぞ! reactjs.org

遅延初期化
初期 state の作成を遅延させることもできます。
そのためにはinit関数を第 3 引数として渡してください。初期stateがinit(initialArg) に設定されます。

これにより初期 state の計算をreducerの外部に抽出することができます。
アクションに応じて state をリセットしたい場合にも便利です。

const init = (initialCount) => {
  return {count: initialCount};
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}
const Counter = ({initialCount})  => {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>

        Reset
      </button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

今回のケースはサンプルなのでuseReducerを使うまでもないですが、 普段のアプリケーションのコードはより複雑なので、useReducerは活躍しそうですね!

おわりに

最後まで読んでいただきありがとうございます!

今回はReact Hooksについてよく使われるであろう、4つを紹介しました!
hooksは他にも沢山あるので、気になる方は公式Documentをご覧ください!

reactjs.org