技術的負債を作らないためのReact Hooks入門


React Advent Calendar 2019 - Qiita の 12/4 です。12/5も終わろうとしていますが、僕の中ではまだ12/4です…。ほんとすみません…。

さて、技術的負債は、常に注意深く設計しない限り、必ず積み重なっていくものです。ウェブフロントエンドを含めたGUI開発では、テストがかかれないことも多いため、技術的負債を作らないために、React Hooks入門記事を書いてみました。

もっと詳しく!とかここが分からない!とかがあればぜひコメントなりいただければ、追記できる気力がある限り追記していきたいと思います。

サンプルコードは https://github.com/erukiti/react-hooks-without-debt に置いています。

React Hooks の基本

React Hooksの考え方や個々のAPI仕様に関しては、公式ドキュメント を読むとわかりやすいと思います。

また、筆者が書いた技術同人誌 Effective React Hooks もあります。

数年前に書いたReact+Redux入門のコンポーネントはES2015+のclass機能を使って書かれていましたが、React Hooksでは関数だけでコンポーネントを書きます。

関数を使う利点は、公式ドキュメントに書いてありますが、シンプルなコードにできることです。

  • 関数にすることでクラス型コンポーネントにまつわる複雑さから開放される
  • ただし、ただの関数ではステートなどを持つことができない(以前はHoCなどのテクニックを使っていた)
  • カスタムフックによりコンポーネント関数をシンプルに保つための関数分割が可能になる

といったところがポイントです。

この記事では、個々のAPI useStateuseEffect については踏み込まないので、それは公式ドキュメントを読んでおくといいと思います。

セットアップ

create-react-appというツールを使えば簡単です。

# yarn
yarn create react-app <project名> --template typescript

# npm
npx create-react-app <project名> --template typescript

カスタムフック

技術的負債を作らないためのReact Hooksの観点で重要なポイントは、カスタムフックと、カスタムフックのテストにあります。React Hooksでは、公式が用意している Hooks 関数だけではなく、自前の Hooks 関数を作成でき、これをカスタムフックと呼びます。

実は、React Hooksを使っても、単に公式の Hooks 関数を使うだけだと、密結合の呪いはクラス型コンポーネントの頃と同じ位には降り掛かってしまいます。それを解決するための方法がカスタムフックなのです。

カスタムフックは useHoge のような、useで始まる関数であり、コンポーネント関数と同じように、Hooks 関数を呼び出すことができます。

custom-hook.ts
import { useState, useCallback } from 'react'

export const useTextInput = (
  init: string = '',
): [string, (e: any) => void] => {
  const [value, setValue] = useState(init)
  const handleChange = useCallback(
    (e: any) => {
      setValue(e.target.value)
    },
    [setValue],
  )
  return [value, handleChange]
}

useTextInput はテキスト入力をするカスタムフックです。やっていることはとても単純で、初期値を元にuseStatevaluesetValueを作成し、handleChangeという関数をuseCallbackで作成し、valuehandleChange のみを返すものです。

App.tsx
import React from 'react'
import { useTextInput } from './custom-hook'

const App: React.FC = () => {
  const [name, handleChangeName] = useTextInput()
  const [favorite, handleChangeFavorite] = useTextInput()
  return (
    <div>
      名前: <input value={name} onChange={handleChangeName} />
      <br />
      好きなもの: <input value={favorite} onChange={handleChangeFavorite} />
    </div>
  )
}

export default App

使い方は<input value={value} onChange={handleChange} />のようにinputタグに渡すだけです。

さて、このカスタムフックはどうすればテストできるでしょうか?

$ yarn add -D @testing-library/react-hooks react-test-renderer
custom-hook.test.ts
import { renderHook, act } from '@testing-library/react-hooks'

import { useTextInput } from './custom-hook'

test('useTextInput', () => {
  const { result } = renderHook(() => useTextInput('hoge'))
  const [value, handleChange] = result.current
  expect(value).toBe('hoge')
  act(() => {
    handleChange({ target: { value: 'fuga' } })
  })
  expect(result.current[0]).toBe('fuga')
})

@testing-library/react-hooksに含まれるrenderHookと、actを使います。

renderHook(() => useTextInput()) のようにカスタムフックを呼び出す関数を引数として渡します。このコードではわかりやすいようにテキストの初期値として hoge を渡しています。renderHookの戻り値の .result.current にはカスタムフック関数の戻り値がそのまま入っているため、さらに value を取り出して、hoge であることを確認します。

あとは、handleChange を直接呼び出すことでカスタムフック関数の、文字入力の処理を実行しています。testing-library/reactreact-hooks では、何かしらコンポーネントに変化をもたらすときには act のコールバックの中で行います。これによりReactのレンダリングを内部的に行っています。カスタムフックを含めたフック関数はすべてReactコンポーネントありきの仕組みなため、このような処理が必要です。

act が完了すると、result.current[0] は、新しい value に置き換わっているため、fuga という値に置き換わっています。

  • renderHook を使うとカスタムフックのテストが可能
  • act コールバック内でReactコンポーネントに変化をもたらす処理を行う

ポイントはこの2点です。

これにより、少なくともカスタムフックのユニットテストは可能になります。今回書き換えたApp.tsxでは、useTextInputの呼び出しと、inputタグの組み立てくらいしか行っていないため、これ以上のテストは、できるとすればE2Eテストか画像回帰テストくらいです。

技術的負債との戦い方

技術的負債が生み出される背景は色々あります。組織論、人員不足などは大きな問題ですが、そういったものはいったんさておき、技術的側面だけで見ると、密結合や低凝集性という設計上の問題、テストが無いなどが主たる原因です。

  • 密結合・低凝集性などといった設計上の誤り
  • テストがない

密結合との戦いについては、SOLID原則に関して筆者が書いた別のブログを御覧ください。

凝集性については今回は省略します。

テスト

技術的負債と戦うためにはテストが必須です。ウェブフロントエンドでも同様です。

密結合をするとテストがしづらくなります。フルスタックフレームワークは密結合になりやすい問題があり、ユニットテストがしづらいケースがとても多いです。クリーンアーキテクチャなどでは、こういった問題に対しては、なるべくフレームワークの決定を遅らせる、依存しすぎないということで、なるべく疎結合を保つべきだとしています。

  • ロジックに対してはユニットテストを書く
    • ロジック(特にビジネスロジック)でユニットテストを書きづらいのならばそれは設計に不備がある(多くの場合は、フレームワークなどに密結合してしまっている)

カスタムフックは基本的にはユニットテストしやすいものです。

ウェブフロントエンドや他GUIにおいては、コンポーネントを Humble Object Pattern というデザインパターンで、Presentation と View を分離しましょう。

  • テストしやすいもの Presentation はユニットテストを書く
  • テストしづらいもの View は、E2Eテストか画像回帰テストなどを書く。もしくは自動テストを諦める

先程の、カスタムフックへの分割と、カスタムフックのユニットテストは、まさにHumble Object Patternです。

まとめ

結局の所、この記事ではあまり複雑なことを主張するわけではなく、React Hooksではカスタムフックを使うことで Humble Object Pattern をやりやすく、技術的負債を貯めないための第一歩にふさわしいということが、主張したいことでした。

  • React Hooksにはカスタムフックという仕組みがある
  • カスタムフックはコンポーネントをテストしやすいものとしづらいもので分離するのに向いている
    • Humble Object Pattern によりテストしやすいPresentationとしづらいViewに分離する
  • Presentationはユニットテストをする
  • Viewは、E2Eテストか画像回帰テストか、あるいは諦めるなどで対処する

サンプルコードは https://github.com/erukiti/react-hooks-without-debt に置いています。