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 useState
や useEffect
については踏み込まないので、それは公式ドキュメントを読んでおくといいと思います。
セットアップ
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 関数を呼び出すことができます。
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
はテキスト入力をするカスタムフックです。やっていることはとても単純で、初期値を元にuseState
でvalue
とsetValue
を作成し、handleChange
という関数をuseCallback
で作成し、value
と handleChange
のみを返すものです。
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
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/react
や react-hooks
では、何かしらコンポーネントに変化をもたらすときには act
のコールバックの中で行います。これによりReactのレンダリングを内部的に行っています。カスタムフックを含めたフック関数はすべてReactコンポーネントありきの仕組みなため、このような処理が必要です。
act
が完了すると、result.current[0]
は、新しい value
に置き換わっているため、fuga
という値に置き換わっています。
-
renderHook
を使うとカスタムフックのテストが可能 -
act
コールバック内でReactコンポーネントに変化をもたらす処理を行う
ポイントはこの2点です。
これにより、少なくともカスタムフックのユニットテストは可能になります。今回書き換えたApp.tsx
では、useTextInput
の呼び出しと、input
タグの組み立てくらいしか行っていないため、これ以上のテストは、できるとすればE2Eテストか画像回帰テストくらいです。
技術的負債との戦い方
技術的負債が生み出される背景は色々あります。組織論、人員不足などは大きな問題ですが、そういったものはいったんさておき、技術的側面だけで見ると、密結合や低凝集性という設計上の問題、テストが無いなどが主たる原因です。
- 密結合・低凝集性などといった設計上の誤り
- テストがない
密結合との戦いについては、SOLID原則に関して筆者が書いた別のブログを御覧ください。
- よくわかるSOLID原則1: S(単一責任の原則)|erukiti|note
- よくわかるSOLID原則2: O(オープン・クローズドの原則)|erukiti|note
- よくわかるSOLID原則3: L(リスコフの置換原則)|erukiti|note
- よくわかるSOLID原則4: I(インターフェース分離の原則)|erukiti|note
- よくわかるSOLID原則5: D(依存性逆転の原則)|erukiti|note
凝集性については今回は省略します。
テスト
技術的負債と戦うためにはテストが必須です。ウェブフロントエンドでも同様です。
密結合をするとテストがしづらくなります。フルスタックフレームワークは密結合になりやすい問題があり、ユニットテストがしづらいケースがとても多いです。クリーンアーキテクチャなどでは、こういった問題に対しては、なるべくフレームワークの決定を遅らせる、依存しすぎないということで、なるべく疎結合を保つべきだとしています。
- ロジックに対してはユニットテストを書く
- ロジック(特にビジネスロジック)でユニットテストを書きづらいのならばそれは設計に不備がある(多くの場合は、フレームワークなどに密結合してしまっている)
カスタムフックは基本的にはユニットテストしやすいものです。
ウェブフロントエンドや他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 に置いています。