Yamamoto Zatsu

やまもとの雑記

React Hooks やっていき

f:id:yt_ymmt:20181226102053p:plain

reactjs.org

React v16.7.0-alphaから実装された機能で、従来のSFC(Stateless Functional Component)におけるstate管理などを便利にするためのユーティリティである。React Hooksを使ったコンポーネントはStatelessではなくなるため、公式でもFC(Functional Component)と呼ばれている。

本記事は自分自身のための勉強用の記事としてポストする。が、これから使い方を覚えたい人にも役にたったら嬉しいためできるだけ整理して書く。

また今回は機能を説明のみで、組み合わせてどう使うかは別で記事を書く予定。

Motivation

先にReact Hooksが生まれた背景を読んでみる。

Wrapper Hell

React Hooksが生まれた背景にはコンポーネントWrapper Hellという言葉が出てくる。これは、HOC(High Order Component)やRender Props、Provider、Consumerなどの抽象化層にコンポーネントが囲まれ、ネストが深い状態を指している。Wrapper Hellはアプリケーションのコードが理解しづらい状態であると述べられている。

React Hooksを使用してstateに関わるロジックを抽出することで、テストしやすいコードを設計できるようになる模様。

(Reactの開発チームはあんまりHOCとかRender Propsとか好きじゃないのかな)

Complex Component

componentDidMountandやcomponentDidUpdateなど、Reactのライフサイクルでは様々なロジックが含まれている。イベントのタイミングごとでそれぞれ分割されるけど、単一メソッドにまとめられる関数もあって、それらはバグの温床になることを懸念に思っているらしい。stateの状態を操作する関数も当然存在するため、テストが困難になる。だから我々はReduxを筆頭としたライブラリを使う。そしてライブラリを使うと上に挙げたWrapper Hellが多くなり、コンポーネントの再利用性に影響する。このスパイラルを軽減したいっぽい。

React Hooksは関連するstate(subscriptionやデータフェッチ)だけをまとめることで最小のコンポーネントを実現する。そしてそれは再利用性が高いものである、と述べている。

Classes confuse

ClassはReactを学ぶ上で大きな混乱を招くと書いてある。その理由としてthisが挙げられていて、this.myFunc = this.myFunc.bind(this)みたいなコードにおけるthisの振る舞いや冗長な書き方に懸念を感じているっぽい。

Prepack Component(事前コンパイラコンポーネント)なんていうのも生まれていて、それらを実行するときにClassを使用したコンポーネントはオーバーヘッドを生んでいる模様。

React HooksはClassコンポーネントを使用することなく同等かそれ以上の機能を提供するものとして定義されている。

所感

色々な背景はこんな感じ。一言でまとめると、Functional ComponentにClass Componentと同等の機能を持たせるものと認識できる。

install

実際に使う場合、今はrc版として提供されている。npm or yarn でインストールする場合は、本日現在16.7.0-alpha.2 を使うことで試すことができる。

yarn add react@16.7.0-alpha.2 react-dom@16.7.0-alpha.2 -S

TypeScriptの型定義もすでに用意されている。(@latestは特に不要)

yarn add @types/react@latest @types/react-dom@latest -S

次にHooksの機能を順番に説明する。

Hooks API

React Hooksには以下のようなAPIが存在する。

Basic Hooks

  • useState
  • useEffect
  • useContext

Additional Hooks

  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImplementsMethods
  • useLayoutEffect

useState

先にコードを見てみよう。これは公式のドキュメントにも掲載されている一番シンプルなコードだ。

import { useState } from 'react';
    
function Example() {
  const [count, setCount] = useState(0);
    
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

この機能を使うとFC(Functional Component)がStateを扱えるようになる。Class Componentで書いていたsetState()をFCで使えるようになったと考えればシンプルである。

使い方は簡単で、useStateinitialStateを引数として渡すと、[state, dispatchAction]として配列が返る。

const [currentState, dispatchAction] = useState(initialState);

stateをUpdateするときはdispatchActionに値を渡せば更新される。このあたりの名前は自分の好きにつけることができる。 結果として特にonClickのあたりは以前に比べシンプルに書けるようになった。

setStateとの違い

FCのuseStateとClass ComponentのsetStateとの違いとして、値をマージしない点が挙げられている。useStateの場合はすべて上書きされるのでオブジェクトなどをinitialStateに突っ込むときは注意が必要

例えばこんなコードがあるとする。

const [user, setUser] = useState({
  name: 'yamamoto',
  age: 33
});
    
return (
  <div>
    <p>name: ${user.name}</p>
    <p>age: ${user.age}</p>
    <input type="text" onChange={(e) => setUser({name: e.target.value})}
  </div>
)

ポイントはinput要素のonChangeでsetUserを実行しているが、nameしか更新していない。これをやるとageは消える。というわけでマージされないことは意識しておきたい。 この問題は、Objectを使う必要がなければuseStateを複数使うことで回避できる。

const [name, setName] = useState('yamamoto');
const [age, setAge] = useState(33);
        
return (
  <div>
    <p>name: ${name}</p>
    <p>age: ${age}</p>
    <input type="text" onChange={(e) => setUser({name: e.target.value})}
  </div>
)

useEffect

useEffectはライフサイクルに影響を与える機能だ。これも公式のコードを拝借。

import { useState, useEffect } from 'react';
    
function Example() {
  const [count, setCount] = useState(0);
    
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
    
return (
  <div>
    <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
  </div>
  );
}

useEffectはClass ComponentにおけるcomponentDidMountcomponentDidUpdatecomponentWillUnmountがFCで置き換わるイメージ。

実行されるタイミング

useEffectが実行されるタイミングは以下。

こうして見るとgetDerivedStateFromPropsと似たタイミングでもある。

reactjs.org

Clean Up

useEffect内で関数をreturnするとクリーンアップの処理として実行することができる。

import { useState, useEffect } from 'react';
    
function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
    
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
    
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
      return function cleanup() {
        ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
      };
  });
    
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

少しシンプルにして例えばこのコードを見てみよう。

useEffect(() => {
  console.log('subscribe');
  console.log('---');
    
  return () => {
    console.log('unsubscribe')
  }
});

何度かuseEffectが呼ばれた場合のログは以下のようになる。

subscribe
---
unsubscribe
subscribe
---
unsubscribe
subscribe
---
unsubscribe
subscribe

一度目は呼ばれないが二度目から毎回unsubscribeが実行されている。こうしてstateがupdateされるたびにCleanUp関数として実行することができる。もちろんcomponentUnMountのタイミングでもこれは実行される。

Skipping Effects

useEffectの第二引数に配列としてstateを渡すことで、渡されたstateの更新がなかった場合(prev === current)、useEffectをスキップできる。

useEffect(() => {
  console.log(count);
}, [count]);

上の場合は第二引数に[count]を渡している。この場合、countに差異がなければuseEffectは実行されず、無駄な処理が発生しない。配列なので当然複数渡すこともできる。その場合、どれかのstateに変更があった場合、useEffectは実行される。

useContext

useContextはContextAPIを便利にするための機能だ。まずは従来のContextAPIを使ったパターン見てみよう。

import {createContext} from "react";
import {render} from "react-dom";
    
const {Provider, Consumer} = createContext();
    
function App() {
  return (
    <Provider value={0}>
      <Display />
    </Provider>
  );
}
    
function Display() {
  return (
    <Consumer>
      {value => <div>{value}</div>}
    </Consumer>
  );
}
    
render(<App />, document.querySelector("#root"));

従来のContextAPIでは、createContextを実行し、ProviderConsumerを引数で受け取る。Providervalueに値を渡すことで、Consumerを経由して受け取れるようになる。Consumerの箇所はRender Propsを使って実際のviewコンポーネントにpropsを渡すことができるようになる。

使い方の秩序は必要ではあるが、子コンポーネントのどこからでもConsumerを呼び出せすことで、該当する値を取得できることから、いわゆるpropsのバケツリレーを解決するための手段でもある。

そして、useContextを使うと以下のようになる。

import {createContext, useContext} from "react";
...

const context = createContext();

...

function Display() {
  const value = useContext(context);
  return <div>The answer is {value}.</div>;
}

...

useContextにcreateContextの引数であるcontextを渡すと値が利用可能になる。Render Propsを使わないため、Consumerが増えてもネストが深くなることはない。

useReducer

useStateに置き換わるもので、state管理にReduxでお馴染みのReducerパターンを扱えるようにしたもの。以下は公式で紹介されているコード。

const initialState = {count: 0};

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

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

useReducerは第一引数にreducer、第二引数にinitialState、第三引数にinitialActionを取る。返り値は[state, dispatch]となっていて、stateはそのまま値が格納されている。dispatchはreducerに渡す関数で、実行したいaction.typeを渡すことでreducerのswitch文で処理され、新たなstateが返るという仕組みだ。initialActionを設定すると、コンポーネントの初回マウント時に自動的に実行するactionを定義することができる。

react-reduxを使っているとconnectを使って、HOCパターンでどこからでもstateを取得することができた。useReducerはあくまでもreducerを使えるようにするまでであり、useStateにreducerを突っ込んだだけだ。

useReduxのような機能が実装されない限り、今のところuseContextと組み合わせることが前提になりそうなのでそれは後述する。

useCallback / useMemo

useCallback / useMemo はメモ化されたcallbackを実行する関数である。

function TodoList({todos, query}) {
  // Render毎に実行される
  const filterd = filterTodos(todos, query);

  // todosかqueryに変更がなければ実行されない
  const filterd = useCallBack(
    () => filterdTodos(todos, query),
    [todos, query]
  );
}

useCallbackは第一引数にinline callback、第二引数に入力配列をとる。

useCallback(inlineCallback, input array);

useMemoも同様。

useMemo(inlineCallback, input array);

useCallbackとuseMemoの違い

関数を返すのがuseCallbackで、値を返すのがuseMemoなので、以下のように認識できる。

useCallback(fn) = useMemo(() => fn) 

メモ化

入力が変更されていない場合、負荷の高い操作のキャッシュ結果を返すことを指す。 渡した配列の値が変更されたときにだけメモ化されたコールバックを返すことで余計な処理を省くことができる。

パフォーマンス観点で、shouldComponentUpdate / PureComponentをFCで実装するイメージだと思われる。

useRef

useRefはコンポーネントへの参照を返す関数だ。公式のコードを見てみよう。

const TextInputWithFocusButton = () => {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    inputEl.current.focus();
  };

  return (
    <div>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  );
};

useRefはnullもしくは参照したいコンポーネントを渡すとrefオブジェクトを返す。

const refObject = useRef(component);

componentがnullで渡すとオブジェクトが返ってくるので、それをコンポーネントのref属性に渡すことで後からでも追加することができる。

useImplementsMethods

useImplementsMethodsはuseRefのイベント実行時に別の処理を挟むことができる。

const TextInputWithFocusButton = () => {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    inputEl.current.focus();
  };

  useImperativeMethods(inputEl, () => ({
    focus: () => {
      console.log('focus');
    },
  }));

  return (
    <div>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  );
};

useImperativeMethodsは第一引数にrefオブジェクト、第二引数に実行したい関数をまとめたオブジェクト、第三引数に入力配列を渡す。

上の例の場合、focusイベントのコールバックを登録しているようなイメージだ。ボタンをクリックした際、inputにfocusが発生するため、console.log('focus')が実行される。 入力配列に値を渡した場合は、値が更新もしくは合致しない限り、実行されることはない。 例えば、[Math.floor(count/2)]のような値を渡せば、countの値が2で割り切れる場合のみ実行される。

useLayoutEffect

これはuseEffectとやってることは全く同じ。ただし全てのDOMがレンダリングされてからはじめて実行される。スクロール位置を取得してDOMを操作したりする必要がなければ特に実行する必要はない模様。基本的にはuseEffectが推奨されている。

おわりに

公式のドキュメントを眺めつつひとまず機能をざっくり解説した。アプリを作って知見ができたらアーキテクトまわりに記事を別でポストしたい。