React Hooks API 所感 v16.7.0-alpha.2

Tue Nov 27, 2018

大人になると味覚が変化すると言いますが、アレは実のところ感じ取れる味の数が少なくなって鈍感になる、つまりピーマンは相変わらず苦いものだが、その苦味に対して鈍感になることで苦味ではない他の部分を感じ取ることができるようになる、ということらしい。

そういった鈍化の一種なのか、個人的に近頃は心踊るニュースというのがなかったのだけど React v16.7 から入る Hooks API が楽しみで仕方ない。これが入ると私の世界がどう変わるか。

As-is

まずSFCで気持ちよくToggleButtonみたいな小さなComponentを書いたとして。

import React from 'react'
export default const ToggleButton = ({ on, onClick }) => (
  <button onClick={onClick}>{ on ? 'on' : 'off' }</button>
)

SFCのままがいいからって、こんなので多重HOCするとかめんどくないですか。

import React from 'react'
import { compose, withState, withHandlers } from 'recompose'
import ToggleButton from '../components/ToggleButton'
const enhance = compose(
  withState('on', 'toggle', false),
  withHandlers({
    onClick: ({ toggle }) => () => toggle(flag => !flag)
  })
)
export default enhance(ToggleButton)

ましてやclassベースで書き直すとか..つらたん。

import React from 'react'

class ToggleButton extends React.Component {
  constructor() {
    super()
    this.state = { on: false }
  }
  onClick() {
    this.setState({ on: !this.state.on })
  }
  render() {
    const { on } = this.state
    const { onClick } = this
    return <button onClick={onClick}>{ on ? 'on' : 'off' }</button>
  }
}

To-be

Hooks API使うとこう書ける。あまりいい例ではないが。あくまでサンプルとして。

import React, { useState, useCallback } from 'react'
import _ToggleButton from '../components/ToggleButton'

export default const ToggleButton = () => {
  const [on, toggle] = useState(false)
  const onClick = useCallback(
    () => toggle(current => !current),
    [toggle]
  )
  return <_ToggleButton on={on} onClick={onClick} />
}

私はReactの良さというのが、より純粋なプログラムとしてかけるところだと思っていて、自分の仕事がデータ構造を作ることにフォーカスされ、DOMという文脈から隔離された潔白の世界でアプリケーションのコードがかけることにエクスタシーを感じる。それでいうとclass MyComponent extends React.Component { ... } みたいなのは、Reactの文脈や作法を意識させられるのであまり書きたくない。かと言ってrecomposeはコード単位が分割されすぎてめんどい。

Hooks APIが来たらその辺の不満が一気に解消される。

そして、Custom Hooks

独自のHooksを定義する場合も書き味がとてもよいから見て欲しい。

globalState的なものを扱うカスタムフックを書いてみた。検証はしていないし多分動かない。userEffectcomponentDidMountに似ていて、返した関数をUnmount時に実行する感じ。

import { useEffect, useState } from 'react'
const STATE = {
  count: 0
}
const LISTENERS = []

function addListener(handler) {
  LISTENERS.push(handler)
  return () => removeListener(handler)
}

function removeListener(handler) {
  LISTENERS.splice(LISTENERS.indexOf(handler), 1)
}

function updateGlobalState(updater) {
  STATE = updater(STATE)
  LISTENERS.forEach(cb => cb(STATE))
}

export default function useGlobalState(mapToState) {
  const [state, setState] = useState(mapToState(STATE));
  useEffect(() => addListener((globalState) => {
    const nextState = mapToState(globalState)
    if (state !== nextState) {
      setState(nextState)
    }
  }))
  return [state, updateGlobalState]
}

これをComponent側で使う。普通のHooksと同じ感じで使える。

import React, { useCallback } from 'react'
import useGlobalState from '../modules/useGlobalState'

const Counter = () => {
  const [count, setGlobalState] = useGlobalState((state) => {
    return state['count']
  })
  const onClick = useCallback(() => {
    setGlobalState((state) => {
      return Object.assign(state, {
        count: count + 1
      })
    })
  }, [setGlobalState])
  return <button onClick={onClick}>up {count}</button>
}

個人的にはreact-redux-firebaseを置換したくて、使った事あるとわかるがあれを使ってしまうと結構firebaseにがっつりロックインされてしまうので、useFirebaseみたいなのを自分で書いてあとで切り離せるようにしておけそうなところもとても良さそうに思っている。HOCだとコンポーネントとコンポーネントの間に挟まる感じなので、コンポーネントツリーが汚染されるんだけど、Hooksの場合はComponentがリッチになる感じなのであまりそういう懸念がなくてよい。

Building Your Own Hooks

以上。マサカリは受け付ける。