ブログのReactを16.8にあげてReact Hooksで書き換えてみた

2019.02.07

2019/02/06にReact16.8がリリースされ、16.7のalphaから入っていたReact Hooksが安定版にやってきました。 そこで今回このブログで使われているReactを16.8に上げて、ステートフルなコンポーネントをReact Hooksを使いFunction Compoenentに書き換えてみました。

React Hooksの解説をした記事はたくさんあると思うので、今回は書き換えてどう変わったかを見ていきましょう。 該当する部分はHeaderの検索フォームです。

プルリクエストだけ見たいかたはこちら。 https://github.com/mottox2/website/pull/39

書き換えのポイント

stateの書き換え

これはいろんな記事で言及されているuseStateを用いて書き換えます。 useStateに初期値を引数として与えると、値と更新用の関数が返ってきます。

const NewComponent = props => {
  const [query, updateQuery] = useState('')

  const handleInput = (e) => {
    updateQuery(e.target.value)
  }

  return <input onChange={handleInput}/>
}

componentDidMountの書き換え

書き換え前のClassComponentです。主にデータの取得とキーボードイベントのつけ外しです。 コンポーネントの各所に処理が散らばってて可読性が低い状態です。

class OldComponent from Component {
  async componentDidMount() {
    const res = await axios.get('/search.json')
    this.setState({ data: res.data })

    this.focusShortcutHandler = this.focusShortcut.bind(this);
    window.addEventListener('keydown', focusShortcutHandler)
  }

  componentWillUnmount() {
    window.removeEventListener('keydown', focusShortcutHandler)
  }

  focusShortcut(e) {
    if (e.keyCode === keyCodes.SLASH && !this.state.isActive) {
      this.focusInput()
      e.preventDefault()
    }
  }
}

useEffectで書き換えると以下のようになります。 useEffectはcomponentDidMount, componentDidUpdateのタイミングで呼ばれて、returnで返した関数がcomponentWillUnmountのタイミングで呼ばれるHookと考えてください。 つまりイベントの付け外しが同じHooksの中で操作出来るので、以前は散らばっていた同じような処理がまとめられ読みやすくなっていると思います。

また、axiosでデータ取得を行っている部分はコンポーネントがマウントされたタイミングでのみ実行したいので第2引数に[]を渡しています。(第2引数は監視したい値を配列で渡す。この場合変更を監視しないという意味でから配列を渡している)

useEffectに関してはReactの公式ドキュメントに詳しく書いてあるので読むといいでしょう。 https://reactjs.org/docs/hooks-effect.html

const NewComponent = (props) => {
  useEffect(() => {
    axios.get('/search.json').then(res => {
      updatePosts(res.data)
    })
    return undefined
  }, [])

  useEffect(() => {
    const focusShortcut = (e: KeyboardEvent) => {
      if (e.keyCode === keyCodes.SLASH && !isActive) {
        inputEl.current.focus()
        e.preventDefault()
      }
    }
    window.addEventListener('keydown', focusShortcut)
    return () => {
      window.removeEventListener('keydown', focusShortcut)
    }
  })
  return <div/>
} 

prevStateを利用するcomponentDidUpdateの書き換え

Hooksの公式Q&Aにもある内容ですが、prevStateとstateを比較して処理を行う部分があってその書き換えにusePreviousというCustom Hooksを利用しました。

もともとのコードがあまり綺麗でないのはご愛嬌…

const usePrevious = (value) => {
  const ref = useRef(null)
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}

const NewComponent = () => {
  const inputEl = useRef(null)
  const prevMobileShow = usePrevious(props.isMobileShow)
  if (prevMobileShow !== props.isMobileShow) {
    window.setTimeout(() => {
      inputEl.current.focus()
    }, 10)
  }

  return  <input ref={inputEl} />
}

useMemoを利用した値のメモ化

今回、Hooksに書き換えたコンポーネントは検索フォームです。言ってしまえば、入力されたクエリに応じて表示する記事をフィルタリングするためのコンポーネントです。 こういったコンポーネントで更新するたびにフィルリングの処理を実行すれば動作は重くなっていきます。

そういったときに利用したいのがuseMemoというフックです。useMemoはいわゆるメモ化を行ってくれるフックで関数の実行結果を保持してくれます。 この場合、posts(記事の配列)とquery(入力されたクエリ)に変化がなければ再計算は必要ないはずなので次のような実装になりました。

const filterPosts = (posts, query) => {
  return posts.filter(item => {
    const itemString = `${item.title} ${item.tags.join('')}`.toLowerCase()
    if (!(itemString.indexOf(query) > -1)) {
      return false
    }
    return true
  })
}

const NewComponent = (props) => {
  const [query, updateQuery] = useState('')
  const [posts, updatePosts] = useState([])
  const filteredPosts = useMemo(() => filterPosts(posts, query), [posts, query])

  return <div />
}

感想

パラダイムシフトを感じた。Viewのすべてが関数で表現出来る世界が近づきつつあるのかもしれない。 これは自分がjQuery文化からReact文化に入った4年前ぐらいの時と同じような衝撃を受けた。 今までVueとReactは大きく変わらないという印象を持っていたが、今回のアップデートで別物という印象に変わった。

React公式のスタンスとしては、React Hooksへの利用を強制するようなものではない…1としているので今まで通りの書き方を続けてもいいが、ぜひReactユーザーにはHooksを触って欲しいと思いました。

注釈

  1. You don’t have to learn Hooks right now. Hooks have no breaking changes, and we have no plans to remove classes from React. https://reactjs.org/blog/2019/02/06/react-v16.8.0.html