Dave Ceddia氏による全5回におよぶReact Hooks入門記事の第3回を本人の許可を得て意訳しました。
誤りやより良い表現などがあればご指摘頂けると助かります。
原文: https://daveceddia.com/usereducer-hook-examples/
新機能React Hooksの中で、名前だけをみると、これが最もホットです。
「reducer」という言葉は、多くの人にReduxのイメージを思い起こさせますが、この記事を読んだり、React v16.7.0 alphaから使える新しい useReducer
フックを使うのにReduxを理解する必要はありません。
「reducer」は実際どんなもので、 useReducer
を使ってコンポーネントの複雑なstateをどう管理するのか、そして、この新しいフックはReduxにとって何を意味するのかについてお話します。Reduxはフックを取り込むのでしょうか?
この記事では、 useReducer
フックを見ていきます。これは useState
の時よりも複雑なstateを管理するのに大いに役立ちます。
メモ:Hooksは現在α版であり、プロダクション環境ではまだ使用できません。APIはさらに変更される可能性があるため、現時点ではプロダクションアプリの書き換えはオススメしません。Open RFCにコメントし、公式ドキュメントやFAQにも目を通してください。
まずはuseReducerフックでできることを説明したビデオチュートリアルをご覧ください。(そのまま読み進んでも構いません)
Reducerとは?
Reduxもしくは配列の reduce
メソッドに精通しているのであれば、「reducer」とは何か?についてはご存知でしょう。ご存知ない方のために説明しておくと、「reducer」とは、2つの値を取り、1つの値を返す関数を意味する素敵な言葉です。
配列があるとして、この中身を1つの値にまとめたい時、「関数型プログラミング」の手法では、配列の reduce
関数を使います。たとえば、数値の配列から合計値を求めたい時は、reducer関数を書いて reduce
に渡します。こんな感じです。
let numbers = [1, 2, 3];
let sum = numbers.reduce((total, number) => {
return total + number;
}, 0);
初見の場合は少し謎めいて見えるかもしれません。これは配列の各要素ごとに関数を呼び出して、前の total
と現在の要素の number
を渡します。何を返そうと新しい total
になります。 reduce
の第二引数(今回は 0
)は total
の初期値になります。この例では、reduceに渡される関数(「reducer」関数として知られる)は3回呼ばれます。
-
(0, 1)
で呼ばれて1を返します。 -
(1, 2)
で呼ばれて3を返します。 -
(3, 3)
で呼ばれて6を返します。 -
reduce
は6を返し、sum
に格納されます。
なるほど、ではuseReducerの役割は?
ページの半分を使って配列の reduce
関数を説明してきたのは、 useReducer
が同じ引数を取り、基本的には同じように動作するからです。reducer関数と初期値(初期state)を渡します。reducerは現在の state
と action
を受け取ります。合計値を求めるreducerのようなものを書いてみましょう。
useReducer((state, action) => {
return state + action;
}, 0);
それで...何が起こるかって? action
はどうやって入ってくるのかって?良い質問ですね。
useReducer
は2つの要素からなる配列を返します。 useState
フックと似ています。1番目は現在のstateで、2番目は dispatch
関数です。実例で見て行きましょう。
const [sum, dispatch] = useReducer((state, action) => {
return state + action;
}, 0);
「state」がどんな値にもなれることに気を付けてください。オブジェクトである必要はありません。数値、配列などどんな値でも可能です。
このreducerを使って数値をインクリメントするコンポーネントの完全な例を見てみましょう。
import React, { useReducer } from 'react';
function Counter() {
// 初回のレンダリングでstateが生成されますが、
// その値は未来のレンダリングにも引き継がれます
const [sum, dispatch] = useReducer((state, action) => {
return state + action;
}, 0);
return (
<>
{sum}
<button onClick={() => dispatch(1)}>
Add 1
</button>
</>
);
}
このCodeSandboxを試してみてください。
ボタンクリックによって、1という値を持った action
がディスパッチされる流れを確認できます。現在のstateに加算され、コンポーネントは新しい(大きな!)stateで再レンダリングされます。
ここでは敢えて「action」が { type: "INCREMENT_BY", value: 1 }
のような形を取らない例を示しました。Hooksで作成するreducerはReduxの典型的なパターンに従う必要がないためです。Hooksの新しい世界では、古いパターンが価値のあるもので、維持されるべきものなのか、変えていくべきものなのかは再考する余地があります。
より複雑な例
典型的なReduxのreducerにより近い例を見ていきましょう。ショッピングリストを管理するコンポーネントを作成します。ここでは新たなフック useRef
も登場します。
まず最初に、2つのフックをimportする必要があります。
import React, { useReducer, useRef } from 'react';
続いて、refとreducerを設定するコンポーネントを作成します。refはフォームの入力項目への参照を保持し、値を抽出することができます。(これまで通り、 value
と onChange
propsを使って、stateで入力項目を管理しても構わないのですが、 useRef
をお披露目する良い機会です!)
function ShoppingList() {
const inputRef = useRef();
const [items, dispatch] = useReducer((state, action) => {
switch (action.type) {
// アクションに応じて何かをする
}
}, []);
return (
<>
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
</form>
<ul>
{items.map((item, index) => (
<li key={item.id}>
{item.name}
</li>
))}
</ul>
</>
);
}
今回の「state」が配列であることに留意していください。空配列で初期化し( useReducer
の第2引数)、reducer関数から即座に配列を返します。
useRefフック
少々寄り道してからreducerに戻りますが、ここで useRef
について説明しておきましょう。
useRef
フックにより、DOMノードへの永続的な参照を作成することができます。 useRef
の呼び出しでは、参照のないオブジェクトを作成します。(引数を渡すことで初期値を設定することもできます) current
プロパティが含まれるため、上記の例では、 inputRef.current
によって入力項目のDOMノードにアクセスできます。 React.createRef()
を触ったことがあれば、ほぼ同じように動作すると考えて良いでしょう。
useRef
によって返されるオブジェクトは、DOMへの参照だけではなく、コンポーネントインスタンス固有の値を持つこともでき、レンダリング間でもその値は保持されます。どこかで聞いたような話ですね!
useRef
は汎用のインスタンス変数を生成するために使われますが、これはクラスコンポーネントで言うところの this.whatever = value
と同じようなものです。注意すべき点としては、それが「副作用」として扱われるため、レンダリング時に更新することはできませんが、 useEffect
フックの中身だけは例外です。( useEffect
についての詳細は明日!)公式のHooks FAQにはrefをインスタンス変数として活用する例があります。
useReducerふたたび
入力項目を form
でラップしたので、エンターキーで送信機能が発火します。 handleSubmit
関数でリストに項目を追加できるようにしたり、reducer内のアクションを処理する関数を書いたりする必要があります。
function ShoppingList() {
const inputRef = useRef();
const [items, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'add':
return [
...state,
{
id: state.length,
name: action.name
}
];
default:
return state;
}
}, []);
function handleSubmit(e) {
e.preventDefault();
dispatch({
type: 'add',
name: inputRef.current.value
});
inputRef.current.value = '';
}
return (
// ... same ...
);
}
reducer関数に2つのケースを追加しました。 actionが type === 'add'
の時のケースと、それ以外に適用される default
ケースです。
reducerは「add」actionを受け取ると、全ての古い要素の末尾に新しい要素を追加した新たな配列を返します。
配列の長さを自動インクリメントされるIDとして活用しています。手っ取り早く目的を達成していますが、IDの重複やバグの原因となりやすいため、実際のアプリケーションではおすすめしません。(uuidのようなライブラリを使うか、サーバー側でユニークなIDを生成するのが良いでしょう!)
handleSubmit
関数は、ユーザーが入力項目上でエンターキーを押した時に呼び出されるため、 preventDefault
でページ全体の更新を抑制する必要があります。ここでactionが dispatch
されます。このアプリケーションでは、Redux方式のアクション、つまり type
プロパティと関連データを持つオブジェクトを採用しています。また、入力をクリアしています。
現時点のプロジェクトはこのCodeSandboxで確認できます。
項目を削除する
続いて、リストから項目を削除する機能を追加してみましょう。
「削除」 <button>
を項目の隣に追加して、 type === "remove"
と削除する項目のインデックスを持ったactionをdispatchします。
後は、reducerでアクションを処理するだけです。配列をフィルタリングして不要な項目を削除します。
function ShoppingList() {
const inputRef = useRef();
const [items, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'add':
// ... 前回と同じ ...
case 'remove':
// 削除対象以外の項目を保持する
return state.filter((_, index) => index != action.index);
default:
return state;
}
}, []);
function handleSubmit(e) { /*...*/ }
return (
<>
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
</form>
<ul>
{items.map((item, index) => (
<li key={item.id}>
{item.name}
<button
onClick={() => dispatch({ type: 'remove', index })}
>
X
</button>
</li>
))}
</ul>
</>
);
}
CodeSandeboxで確認してみてください。
練習:リストをクリアする
もう1つ機能を追加してみましょう。リストをクリアするボタンです。練習あるのみです!
<ul>
の上にボタンを挿入して、 onClick
propを設定します。これはtypeが「clear」のactionをdispatchします。次に、reducerにケースを追加して「clear」actionを処理します。
前のCodeSandboxのチェックポイントを開いて更新してみてください(安心してください。私のsandboxは上書きされません)。
...
...
上手く行きましたか?さすがですね!
答えだけを見ようとしてスクロールしてしまいましたか?このHooksは自分自身で少しでも試してみれば魔法ではないことが理解できるので、ぜひ試してみてください!
つまり...Redux終了のお知らせ?
多くの人々は最初に useReducer
フックを見た時にこんなことを考えるでしょう。「なるほど、Reactはビルトインのreducerとデータを引き回すためのContextを獲得したので、Reduxは死んだってことだな!」きっと疑問に感じるでしょうから、私の考えを述べておきます。
個人的に、 useReducer
は、ContextがReduxを殺した(ってことはない)ほどReduxに致命傷与えるものではないと考えています。とはいえ、状態管理におけるReactの機能拡張により、Reduxが本当に必要なケースは減って行くでしょう。
ReduxはContextと useReducer
の結合以上のことを行います。Redux DevToolsでの素晴らしいデバッギング、カスタム性の高いミドルウェアと、ヘルパーライブラリのエコシステム全体などです。Reduxはやり過ぎとも思えるような場所(私の例も含めて、その使用法を教えるほとんどすべての例で)で使われることが多いと言っても良いでしょう。それでもまだ強い存在意義があります。
Reduxがグローバルなstoreを提供し、アプリケーションのデータを集中管理するのに対し、 useReducer
は特定のコンポーネントにローカライズされています。あなた自身のミニReduxを useReducer
と useContext
で構築するのを妨げるものはありません!そうしたければ、また必要であれば、やってみてください!(Twitterでは多くの開発者が既にそうして共有したりしています)個人的には、DevToolsがなくて残念ですが、間もなく5〜10もしくは300ものnpmパッケージがそれを解決するために登場するでしょう。
一言で言えば、Reduxは死にません。HooksはReduxを廃れさせることはありません。それは時が経てばはっきりするでしょう。Hooksは数日前に登場し、まだアルファ版です!コミュニティがこの新機能を使って何を作るのかを考えるだけでワクワクします。
実際に試してみましょう!
useReducer
フックを試すのにちょうど良い小さなアプリケーションのアイデアをご紹介します。
- 4つのレベルを持った照明付きの「部屋」を作ってみましょう。オフ、低、中、高のレベルを持ち、ボタンを押すたびに更新されます。
- 正しい順番で押されるとアンロックされる6個のボタンを持つ「キーパッド」を作成してみましょう。正しいボタンを押すたびにstateは更新され、間違ったボタンを押すとリセットされます。