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

JavaScriptのreduce()とは

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

JavaScriptのreduceは配列のメソッドで、配列の要素を1つずつ取り出すloop処理の中で指定した関数(コールバック関数)を適用し新しい値を戻します。

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

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

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を作成することができます。


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の値は[]になります。

useReducerとreduce

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

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

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


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

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

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


// reduce
(previousValue, currentValue) => nextPreviousValue

//useReducer
(state, action) => newState

useReducerの使い方

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

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


const [state, dispatch] = useReducer(reducer, 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;

reducerもinitialStateも定義されていないのでブラウザで表示させようとしてもreducerとinitialStateの未定義エラーが発生して表示することができません。未定義を解決していくために最初にinitialStateを設定します。initialStateの中ではCounterコンポーネント内で管理したい変数の初期値を設定します。ここでは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で行いたい処理に合わせて下記のように記述することができます。

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


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

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倍にするDOBLE_INCREとRESETを追加しています。


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