React Native で React Hooks を使うにあたっての考察
これは Farmnote Advent Calendar 9 日目の記事です。
TL;DR: アプリの目的やライフサイクル、投資可能なコストを意識して使うかどうかを決めよう
前回、 React Native で React Hooks を使うための方法を紹介しましたが、すべてを Hooks で置き換えるべきか ? という問いを考えたたのでつらつらと書き連ねてみます。
Basic Hooks & Additional Hooks
React Hooks の API リファレンスでは Basic なものと Additional なものとにわかれています。
Basic Hooks は次です。
- useState
- useContext
- useEffect
対して Additional Hooks は現在提供されているもののうち、 Basic に属するもの以外となっています。これに関しては次のような説明がなされています。
The following Hooks are either variants of the basic ones from the previous section, or only needed for specific edge cases. Don't stress about learning them up front.
( 超訳 ) Additional Hooks は Basic Hooks とは少し異なります。エッジケースでのみ必要となるものです。現在はこれらの習得についてあまり気に病まないでください。
最初、このメッセージに気づかずに触ってみたのですが、「これは使いづらい」という感想を持ちました。では Additional Hooks のうち、私の独断と偏見で実際に使ってみた useReducer 、 useCallback / useMemo について、どんなものなのか書いてみます。
useReducer
useReducer は Flux データフローの流れを汲む API です。 Flux については公式サイトをみていただくとして、コードを見たほうが一目瞭然でしょう。
import React, { useReducer } from 'react'
import {
Button,
Text,
View,
} from 'react-native'
const initialState = 0
const reducer = (state, action) => {
switch (action.type) {
case 'reset':
return initialState
case 'increment':
return state + 1
case 'decrement':
return state - 1
default:
return state
}
}
export default ({initialCount}) => {
const [state, dispatch] = useReducer(reducer, initialCount)
return (
<View>
<Text>Count</Text>: state
<Button
title="Reset"
onPress={() => dispatch({type: 'reset'})}
/>
<Button
title="+"
onPress={() => dispatch({type: 'increment'})}
/>
<Button
title="-"
onPress={() => dispatch({type: 'decrement'})}
/>
</View>
)
}
useReducer を使っているのは次ですね。
const [state, dispatch] = useReducer(reducer, initialCount)
useReducer ごとに store が作成されます。返り値として現在の状態 state と store の変更用関数 dispatch が渡されます。その他は Flux データフローライブラリーを用いた際の React Component と同様ですね。
useReducer の挙動・制限として次をおさえておくとよいでしょう。
1. useReducer ごとに store が作成される
2. React Hooks API は React Functional Component 内からしか呼び出せない
これらを素直に捉えると useReducer は単一コンポーネント内で使われることを想定した API となります。
また、データフロー、つまりビジネスロジックがコンポーネントと密結合しやすくメンテナンスが難しくなる問題があります。こちらは状況によってはより深刻です。この点については後述します。
他にも素のままではstate / dispatch を特定のコンポーネントに渡すためには地道にバケツリレーする必要があるという課題があるのですが、これを解消するために react-redux パッケージで提供されている connect の概念を導入してみた repository を作ってみました。意図通りに動いているようです。
useCallback / useMemo
useCallback / useMemo ともにメモ化するために使うものです。 useCallback は関数のメモ化、 useMemo は値のメモ化という違いがありますが、両方とも時間がかかる処理の結果をメモリーに保持しておくことで時間短縮を測るものと捉えればよいでしょう。
用途としてはざっと次が挙げられます。
- ネットワーク通信結果をキャッシュする
- CPU 使用率の高い計算結果をキャッシュする
これはまさにコンポーネント内にロジックを閉じ込めるためのものですね。先述したことと同様の問題があります。
React Native が選択される文脈
React Native が選択される場合、理由は 2 つに大別されると感じています。
1. メンテナンスが求められないアプリを時間をかけずに作成したい
2. 必要なスキルセットをしぼることで投資効率を上げたい
1 番はたとえば特定の日時のみ有効なキャンペーンアプリや市場調査などを目的とした実験的なプロトタイプアプリです。対して 2 番は長く使用され、メンテナンスされるアプリではあるものの、投資できる額が制限されている場合です。
Farmnote は自社開発をしているスタートアップですので 2 番の文脈にあたります。 1 番の文脈はたとえば広告代理店さんがキャンペーンアプリを作成する場合などでしょう。
これらの点から、投入する技術が異なってきます。1 番の文脈だと扱うデータ量も多くないでしょうし、ビジネスロジックがコンポーネントに組み込まれていても問題は軽微となります。 Additional Hooks として紹介されている API はニーズにマッチするでしょう。
逆に 2 番の文脈からすると Additional Hooks を用いて作成したコンポーネントはとても許容できないものとなります。 DDD の言葉を借りると smart UI となり、モデル、つまりドメインロジックを育てることができなくなってしまいます。
Facebook の狙い
では Additional Hooks はどういった意図で導入されたものなのでしょうか ?いろいろ考えてみた結果、「Facebook はユーザーの声に真摯に対応しようとしているだけなのではないか ?」という結論に至りました。
もともと React.js は Web サービスを開発するために作られたものなので、そのニーズが吸収されたという点がまず大きいでしょう。
ただ、先述した文脈は両方とも、確かにあるニーズです。ただ、いままでが 2 番に寄りすぎていたのではないか ? それを是正しようとしているのでは ? と、ふと思ったのです。 Redux やそれに付随するパッケージは明らかに 2 番の文脈で効果を発揮しますが、 1 番の文脈では速度が上がらず使いにくいものだったでしょう。最近の Expo チームの合流も 1 番の文脈にいる開発者も救いたい、というメッセージだとも解釈できます。
Facebook 自体の事情はというと、もともと React Native は Facebook アプリを作るために作られたものでした。 2 番の文脈ですね。しかし昨今では ReactConf などのキャンペーンアプリ作成にも使用されているようですし、 1 番の文脈でも Facebook としてメリットがあるのでしょう。
大切なことはアプリの要求を的確に捉えて必要な技術を適宜投入すればよい、ということに尽きるのではないでしょうか。
まとめ
Farmnote が扱うドメインは 10 年単位です。メンテナンス性は何よりも求められます。というわけで useState / useContext / useEffect は使うと思いますが、今のままの Additional Hooks には手を出さないでしょう。