useReducerはuseStateと同様に状態管理 (state management)を行うことができるHookです。useStateでは名前にState(状態)が入っているので状態管理を行うものであるということは容易に想像できますがReducerという言葉から状態管理という言葉を連想するのは難しいはずです。本文書で実際のコードの中でuseReducerを使って動作確認を行うことでuseReducerが状態管理できることを学習していきます。

本文書を含めuseReducer, useStateはどちらも同じ状態管理を行うHookなのでどちらを利用してもコードを記述することはできます。なぜ状態管理を行うのに2つもHookがありuseReducerはわかりにくいのかと思っている人もいるかと思います。同じ状態管理ですが行いたい処理の内容によって使い分けが必要になります。本文書ではどちらのHookがどのような条件に適しているかなどについては言及していないのでuseReducer, useStateの利用や他の人のコードの使用方法を見ていく中でどちらのHookの利用が適しているのかを学んでいく必要があります。そのためにはuseReducer Hook出会っても対応できるように準備しておく必要があります。

そもそもuseReduerの説明を行うのにシンプルなカウンターを利用するのがよくないのかもしれません。カウンターであればuseStateでも簡単に記述できるためuseReducerの複雑さだけが際立ち同じことができるかならuseReducerは使わなくていいかなとなるのかもしれません。私も最初はそう思いました。

コンポーネント間でデータを一元管理するReduxでもReducerは利用されています。Reactを使いこなすためにはReducerを使い方を理解することが重要になるのでまずはuseReducer Hookの使い方の基礎をしっかりマスターしてください。

JavaScriptのreduce()とは

JavaScriptにはreduce()という配列の関数が存在します。useReducerの使用方法を確認する前にJavaScriptのreduce()を確認しておきます。JavaScriptのreduceの使用方法がわかっていない場合でもuseReducerが理解できないわけではありませんがこの機会にぜひreduceの使用方法もマスターしてください。

JavaScriptのreduceは配列の関数で、配列の要素を1つずつ取り出すloop処理の中で指定した関数(コールバック関数)を適用し新しい値を戻します。loopすることを考えるとmapやforEachと同じ?と思われたかもしれませんが下の例で違いを確認していきましょう。

reduceを使って配列の合計

言葉だけでは理解が難しいと思うので配列の要素を合計するという簡単な例を使って説明していきます。JavaScriptの入門者にもわかるように記述方法を3つの方法で記述していますが結果は同じです。馴染みのある記述方法を使って動作の流れを確認してください。


const test = [10,20,30]

// arrow アロー関数を利用した場合
const sum = test.reduce((previousValue, currentValue) => previousValue + currentValue)

// arrow 関数を利用しない場合
const sum = test.reduce(function(previousValue, currentValue){
    return previousValue + currentValue
})

// コールバッグ関数を渡した場合
const callback_func = (previousValue, currentValue) => previousValue + currentValue

const sum = test.reduce(callback_func)

console.log(sum) // 60

上記の例では配列testの要素の合計を計算しています。reduceを利用するとtest配列の要素を1つずつ取り出しながら下記のようにloop処理が行われます。

1回目のループではpreviousValueの値はtest配列の最初の値であるtest[0]が入り、currentValueは次の要素であるtest[1]が入ります。previousValue + currentValueの和であるtest[0] + test[1] = 30が次のループに利用されます。

2回目のループではpreviousValueの値が1回目のループで実行したpreviouseValue(=test[0]) + currentValue(=test[1])の和である30が入ります。currentValueの値は次の配列の要素であるtest[2]が入ります。previousValue + currentValueの和である30 + test[2] = 60が戻され、合計値は60となります。2回のループで完了します。

forEach関数を利用して配列の合計を出すこともできます。


const test = [10, 20, 30];

let sum = 0;

test.forEach((num) => {
  sum = sum + num;
});

console.log(sum);

初期値を与えて配列の合計

reduce()には初期値を与えることもできるので先程の例に初期値100を追加します。


const test = [10,20,30]

const sum = test.reduce(((previousValue, currentValue) => previousValue + currentValue),100)

console.log(sum) //160

初期値を与えると先程とは1回目のループのpreviousValueが変わります。初期値を与える前はpreviousValueの値はtest[0]でしたが、初期値を与えると初期値がpreviousValueの値に入ります。currentValueにはtest[0]が入ります。2回目のループはpreviousValue(初期値+test[0]の合計)でcurrentValueはtest[1]が入ります。test[2]がまだ使われていないので3回目のループも行われます。初期値を与えると3回ループが行われることになります。

構文は下記の通りとなります。

配列.reduce(関数[, 初期値])

reduceには日本語に減少するや整理するという意味がありますが、配列をreduceを使用して合計値という一つの値にすることができたのでこの例だけを考えるとreduceという意味と動作が一致すると思えなくもありません。

オブジェクト構造を使った例

配列の値の合計だけではなくオブジェクト構造を持つ配列にもreduce()を利用することができます。

下記のようは構造を持つ配列usersがあります。


const users = [
    { name: 'John', email: 'john@example.com'},
    { name: 'Kevin', email: 'kevin@test.com'},
    { name: 'David', email: 'david@example.info'}
]

配列usersからメールアドレスだけの配列を作成したいとします。


const mailAddresses = ['john@example.com','kevin@test.com','david@example.info']

reduce()を使って下記のように記述すると新しいオブジェクトmailAddressesを作成することができます。concatは配列を結合することができるメソッドです。concatはconcatenateの短縮で連結という意味を持ちます。


const mailAddresses = users.reduce((previousValue, currentValue) => {
    return previousValue.concat(currentValue.email)
},[])

console.log(mailAddresses)
// [ 'john@example.com', 'kevin@test.com', 'david@example.info' ]

初期値に空の配列[]を指定しているので1回目のループのpreviousValueの値は[]になります。空の配列にemailを追加していくという動作です。

先程まではすべてのemailアドレスを取得しましたがjohn@example.comのメールアドレスは必要ないとい場合は下記のようにreducerを使って処理することができます。


const mailAddresses = users.reduce((previousValue, currentValue) => {
  if (currentValue.email == "john@example.com") {
    return previousValue;
  } else {
    return previousValue.concat(currentValue.email);
  }
}, []);

console.log(mailAddresses)
// ["kevin@test.com", "david@example.info"]

map関数でも同様のことは可能です。


const mailAddresses = users.map((user) => {
  return user.email;
});

console.log(mailAddresses)
// [ 'john@example.com', 'kevin@test.com', 'david@example.info' ]

useReducerとreduce

reduceの使い方を確認したので、userReducerとreducerがどのような共通点を持っているかを確認しておきます。

useReducerの中でJavaScriptの配列.reduce()が利用されているわけではありません。

reduceとuseReducerの構文は下記のようになります。


// reduce
配列.reduce(関数[, 初期値])

//useReducer
useReducer(関数[, 初期値])

reduceではpreviousValueにcurrentValueと関数を利用して新しいnextPreviousValueを作成したようにuserReducerではstateにactionというのstateに関連する処理を加えてnewStateを作成します。どちらも2つ値を使って1つの値にするという共通点があります。


// reduce
(previousValue, currentValue) => nextPreviousValue

//useReducer
(state, action) => newState

useReducerの使い方

実際にReact内でuseReducerを利用して理解を深めていきましょう。

useReducerの構文は下記の通りです。この時点でこれが何を表しているかわからなくても大丈夫です。ボタンをクリックすることで数字が増減するCounterを作成しながら一つ一つ説明を行なっていきます。


const [state, dispatch] = useReducer(reducer, initialState);

もう一つ状態管理のReact HookのuseStateの構文は下記の通りです。


const [state, setState] = useState(initialState)

Counterコンポーネント作成

App.jsが保存されているディレクトリ下にcomponentsディレクトリを作成し、その下にCounter.jsファイルを作成します。


import React from 'react';

function Counter() {
  return (
    <div>
        <h1>Counter</h1>
    </div>
  );
}

export default Counter;

Counterの数を表示するタグとCounterを増減するボタンを追加します。


import React from 'react';

function Counter() {
    return (
        <div className="">
            <h1>Counter</h1>
            <h2>カウント: ここにカウンタ数表示</h2>
            <button>+</button>
            <button>-</button>
        </div>
  );
}

export default Counter;

ブラウザで確認すると下記のような表示になります。ボタンをクリックしても何も変化はありません。

カウンター
カウンター

useReducerの追加

useReducerの構文をそのままCounterコンポーネントの中に追加します。useReduerを利用する場合は、useReducerをimportも行う必要があります。


import React, { useReducer } from 'react';

function Counter() {
    const [state, dispatch] = useReducer(reducer, initialState)
    return (
        <div className="">
            <h1>Counter</h1>
            <h2>カウント: ここにカウンタ数表示</h2>
            <button>+</button>
            <button>-</button>
        </div>
  );
}

export default Counter;

useReducerの引数であるreducerもinitialStateも定義されていないのでブラウザで表示させようとしてもreducerとinitialStateの未定義エラーが発生して表示することができません。未定義を解決していくために最初にinitialStateを設定します。initialStateはCounterコンポーネント内で状態管理を行いたいstateの初期値を設定します。この初期値を設定したstateを更新することになります。ここではcountは0と設定します。Counterコンポーネント内では、countの値はstate.countで取得することができます。


import React, { useReducer } from 'react';

const initialState = {
    count: 0
}

function Counter() {
    const [state, dispatch] = useReducer(reducer, initialState)
    return (
        <div className="">
            <h1>Counter</h1>
            <h2>カウント: { state.count }</h2>
            <button>+</button>
            <button>-</button>
        </div>
  );
}

export default Counter;

initialStateを設定してもreducerが未定義なので引き続きブラウザではエラーが表示されます。

reducerの設定

次にreducerの設定を行う必要がありますがreducerは下記の形式で作成を行う必要があります。stateとactionを引数とし入力すると新しいnewStateが戻されます。


(state, action) => newState

上記の表記をcounterで行いたい処理に合わせて下記のように記述することができます。


const reducer = (state, action) => {
    
    if(action === 'INCREMENT'){
        return {count: state.count + 1}
    }else{
        return {count: state.count - 1}
    }
}

actionの値により処理をする内容が異なります。actionがINCREMENTの場合はstate.countが1増え、actionがINCREMENT以外の場合は1減る設定としています。stateがreducerの処理を通して別のstate(newState)に更新されていることがわかります。

reducerの定義が完了するとエラーがなくなり、ブラウザ上でcountが0と表示されます。もしinitialStateのcountの数字を変更すると0ではなく設定した数字が表示されます。

カウンターが表示
カウンターが表示

ここまでの設定でuseReducerを使用するための設定は完了です。

クリックイベントとdispatchの設定

最後にボタンにonClickイベントを設定し、ボタンをクリックするとカウントが増えるか減るかの処理を追加する必要があります。onClickでは処理したい関数を指定する必要がありますがuseReducer内のstate.countはどのように更新を行えばいいのでしょう。

state内の値を更新するためにはdispatchを利用します。またdispatchに入れる引数にはreducerで設定したactionを指定する必要があります。


<button onClick={() => dispatch('INCREMENT')}>+</button>
<button onClick={() => dispatch('DECREMENT')}>+</button>

クリックイベントを設定して、ボタンをクリックするとCounterの数が増減することが確認できます。

ボタンクリックで数が増減する
ボタンクリックで数が増減する

シンプルなCounterコンポーネントを利用してuseReducerの使用方法を理解することができました。

actionを増やす

INCREMENTとDECREMENTの2つでしたが、reducer内のactionの分岐を増やすことで複数のactionを登録することができます。これまではif文で設定していましたが、switch文に変更を行い、数を2倍にするDOUBLE_INCREとRESETを追加しています。if文でも対応できますが条件分岐が増えてるとコードが読みづらくなりますがswitchの場合だと条件が増えた場合でも比較的コードがすっきりします。


import React, { useReducer } from 'react';

const initialState = {
    count: 2
}

const reducer = (state, action) => {

    switch(action){
        case 'INCREMENT':
            return {count: state.count + 1}
        case 'DECREMENT':
            return {count: state.count - 1}
        case 'DOUBLE_INCRE':
            return {count: state.count * 2}
        case 'RESET':
            return {count: 0}
        default:
            return state
    }
}

function Counter() {
    const [state, dispatch] = useReducer(reducer, initialState)
    return (
        <div className="">
            <h1>Counter</h1>
            <h2>カウント: { state.count }</h2>
            <button onClick={() => dispatch('INCREMENT')}>+</button>
            <button onClick={() => dispatch('DECREMENT')}>-</button>
            <button onClick={() => dispatch('DOUBLE_INCRE')}>++</button>
            <button onClick={() => dispatch('RESET')}>0</button>
        </div>
  );
}

export default Counter;

Actionにtypeとpayloadを追加

ここまでのCounterではreducerの中で増減をする値は1を指定していましがreducerの外側から増減する値を更新できるようにコードの更新を行います。

dispatchでINCREMENTやDECREMENTの文字列を指定していましが、文字列からオブジェクトの指定へ変更します。


<button onClick={() => dispatch({type: 'INCREMENT', payload: 5})}>+</button>
<button onClick={() => dispatch({type: 'DECREMENT', payload: 5})}>-</button>

reducerの変更も必要となります。actionがオブジェクトとなるのでオブジェクトのプロパティをつけて値を取得する必要があります。


const reducer = (state, action) => {

    switch(action.type){
        case 'INCREMENT':
            return {count: state.count + action.payload}
        case 'DECREMENT':
            return {count: state.count - action.payload}
        default:
            return state
    }
}

Counterの見た目は変わりませんが、ボタンを押すと数字が5ずつ増減します。

Counter
Counter

説明とコードを追うことでuseReducerの利用方法はわかったかと思いますが利用するのが簡単とは言い難いHookだったのではないしょうか。冒頭でも述べたようにuseReducerのreducerの処理はReduxなどにも利用されているのでReactをつないこなす上で重要な知識です。

useReducerはuseContextを利用することでコンポーネント間のデータ共有にも利用することができます。