BLOG

React/useReducerについて

useReducerとは

・状態管理用のhookである

・useStateに代替する(useStateはuseReducerで内部実装されている)

・useReducerは、現在のステート値とdispatch関数の2つを返す

const [state, dispatch] = useReducer(reducer,初期値);

(reducerはステートを更新する為の関数)

・dispatch(action)で実行する(actionは何をするのかを示すオブジェク=> {type: increment, payload: 0})

useReducerを使ってカウントアプリを作る

・src/components/CounterReducer.jsを作成する。

・rfceスニペット

・useReducerをインポート

・useReducerにはuseReducer関数と初期値を設定するので、initialStateとreducerを宣言。

・reducer関数にはstateとactionを渡し、新しいステートを返すように実装する。

・reducer内にはカウントのincrement,decrement,reset処理を書き、どの処理を実行するかをactionを渡すことで判断する。

・作成したreducer関数とステートをuseReducerに渡す。

・useReducerはstateとdispatch関数のペアを返すので、それぞれ分割代入する。

・jsx内にはそれぞれのアクションを実行するボタン、カウント数を表示していく。

・dispatch関数に渡しているincrementがactionに当たる。

・App.jsで読み込む。

CounterReducer.js

import React, { useReducer } from 'react'

const initialState = 0
const reducer = (state, action) => {
    switch(action) {
        case 'increment':
            return state + 1;
        case 'decrement':
            return state - 1;
        case 'reset':
            return initialState
        default:
            return state
    }
}
function CounterReducer() {
    const [count, dispatch] = useReducer(reducer, initialState)
    return (
        <div>
            <h1>カウント: {count}</h1>
            <button onClick={() => dispatch('increment')}>increment</button>
            <button onClick={() => dispatch('decrement')}>decrement</button>
            <button onClick={() => dispatch('reset')}>Reset</button>
        </div>
    )
}

export default CounterReducer

App.js

import React, {createContext, useState} from 'react';
import './App.css';
import CounterReducer from './components/CounterReducer';


function App() {

  return (
    <div className="App">
      <CounterReducer/>
    </div>
  );
}

export default App;

無事プラスマイナスできるカウントアプリが完成。

このように、dispatch関数の引数にそれぞれのアクションを渡すことでreducer内の処理を実行できるようになった。

複数の処理を扱う

・firstCounterとsecondCounterを用意する。

・count.firstCounterに変更

・カウント2を追加

・次にdispatchで渡しているactionを変更する。

・今回はactionの中にペイロードを含んでみる。

・まずactionをオブジェクトにし、typeとvalueをセットする、こうすることでincrement数を動的にする。

・decrementも動的にする。

・次にsecondCounter用にボタンを追加する。(10ずつ追加)

・今回はactionにtypeをvalueをセットしているので、switch文のactionをaction.typeに変更。

・secondCounter用にcaseを増やす。

・今回は複数のステートを含んでいて、この場合useStateでも行ったようにスプレッド構文を使って更新前のステートを展開し、オブジェクトのマージを行う。

CounterReducer.js

import React, { useReducer } from 'react'

const initialState = {
    firstCounter : 0,
    secondCounter : 10
}

const reducer = (state, action) => {
    switch(action.type) {
        case 'increment1':
            return { ...state, firstCounter: state.firstCounter + action.value}
        case 'decrement1':
            return { ...state, firstCounter: state.firstCounter - action.value}
        case 'increment2':
            return { ...state, secondCounter: state.secondCounter + action.value}
        case 'decrement2':
            return { ...state, secondCounter: state.secondCounter - action.value}
        case 'reset':
            return initialState
        default:
            return state
    }
}
function CounterReducer() {
    const [count, dispatch] = useReducer(reducer, initialState)
    return (
        <div>
            <h1>カウント: {count.firstCounter}</h1>
            <h1>カウント2: {count.secondCounter}</h1>
            <button onClick={() => dispatch({type: 'increment1', value: 1})}>increment1</button>
            <button onClick={() => dispatch({type: 'decrement1', value: 1})}>decrement1</button>
            <button onClick={() => dispatch({type: 'increment2', value: 10})}>increment2</button>
            <button onClick={() => dispatch({type: 'decrement2', value: 10})}>decrement2</button>
            <button onClick={() => dispatch({type: 'reset'})}>Reset</button>
        </div>
    )
}

export default CounterReducer

useReducerがuseStateより好ましいのは、複数の値にまたがる複雑なステートロジックがある場合や、前のステートにもとづいて、次のステートを決める必要がある場合である。

useReducer + useContextを組み合わせる

カウントアプリを作りつつ、グローバルステートにカウント値をもたせ、各コンポーネントでカウントの参照、更新処理をしていく。

・src/components/ComponentA、ComponentB、ComponentC、を作る。

・rfceスニペット

・これら3つをApp.jsにインポート

App.jsにて

・initialStateとreducerの宣言をする

・useReducer, createContext, useStateを読み込む

・useReducerにinitialStateとReducerを渡すことで、countStateとdispatch関数を作成する。

・createContextを使って、countContextを作成。

・countContextをエクスポート

・countContextを使って、Providerを用意

・valueにcountとdispatchを用意

共有したいpropertiesをvalueにもたせ、Providerでコンポーネントを囲うことによって、コンポーネント間でpropertiesを共有することができる。

・ComponentA.jsにて、コンテキスを使用する為にReactからuseContextをインポートする。

・App.jsで作成したCountContextをインポート

・useContextを使って、Providerでセットしたvalue値を取得する。

・ボタンを作成

・同様に、ComponentB.js、 ComponentC.jsも記述する。

・App.jsにてカウントの表示を追加する。

ComponentA.js

import React, { useContext} from 'react'
import { CountContext } from '../App'

function ComponentA() {
    const countContext = useContext(CountContext)
    return (
        <div>
            <button onClick={() => countContext.countDispatch({type: 'increment1', value: 1})}>increment1</button>
            <button onClick={() => countContext.countDispatch({type: 'decrement1', value: 1})}>decrement1</button>
            <button onClick={() => countContext.countDispatch({type: 'reset'})}>Reset</button>
        </div>
    )
}

export default ComponentA

ComponentB.js

import React, { useContext} from 'react'
import { CountContext } from '../App'

function ComponentB() {
    const countContext = useContext(CountContext)
    return (
        <div>
            <button onClick={() => countContext.countDispatch({type: 'increment1', value: 1})}>increment1</button>
            <button onClick={() => countContext.countDispatch({type: 'decrement1', value: 1})}>decrement1</button>
            <button onClick={() => countContext.countDispatch({type: 'reset'})}>Reset</button>
        </div>
    )
}

export default ComponentB

ComponentC.js

import React, { useContext} from 'react'
import { CountContext } from '../App'

function ComponentC() {
    const countContext = useContext(CountContext)
    return (
        <div>
            <button onClick={() => countContext.countDispatch({type: 'increment1', value: 1})}>increment1</button>
            <button onClick={() => countContext.countDispatch({type: 'decrement1', value: 1})}>decrement1</button>
            <button onClick={() => countContext.countDispatch({type: 'reset'})}>Reset</button>
        </div>
    )
}

export default ComponentC

App.js

import React, {useReducer, createContext, useState} from 'react';
import './App.css';
import ComponentA from './components/ComponentA';
import ComponentB from './components/ComponentB';
import ComponentC from './components/ComponentC';

export const CountContext = createContext()
const initialState = {
  firstCounter : 0,
}

const reducer = (state, action) => {
  switch(action.type) {
      case 'increment1':
          return { ...state, firstCounter: state.firstCounter + action.value}
      case 'decrement1':
          return { ...state, firstCounter: state.firstCounter - action.value}
      case 'reset':
          return initialState
      default:
          return state
  }
}
function App() {
const [count, dispatch] = useReducer(reducer, initialState)
  return (
    <div className="App">
    <h1>カウント: {count.firstCounter}</h1>
    <CountContext.Provider
      value={{countState: count, countDispatch: dispatch}}>
      <ComponentA />
      <ComponentB />
      <ComponentC />
    </CountContext.Provider>
    </div>
  );
}

export default App;

無事にプラスマイナスリセットボタンが3つ並んだカウントアプリが出来る。

useReducer 外部APIからdataを取ってくる。

useReducerとuseEffectを使って、外部APIとやり取りをし、記事の取得と表示を行う。

・App.jsにてReactからuseReducer, useEffectを読み込む

現場でよく使われている、httpリクエストを簡単に行えるようにするライブラリのaxiosを使う

・yarn add axios でインストールしてReact内にインポート

・initialStateを作成

・reducerを作成

・reducerにはstateとactionを渡し、新しいstateを返すように実装。

・今回はデータの取得が成功したcaseのFETCH_SUCCESSと失敗したcaseのFETCH_ERRORをcase文にいれる

・FETCH_SUCCESSにはまずloadingをfalseにし、postにはactionで渡されるpayloadを代入する。今回は成功したcaseなのでerrorの中にはemptyStringを入れる

・FETCH_ERRORにはloadingをfalse、postの中身にemptyオブジェクト、errorの中身にエラーメッセージを仕込む

・defaultではstateを返す。

・initialStateとReducer関数の準備ができたので、これらをuseReducerに読み込み、stateとdispatchを用意する。

・useEffect内にhttpリクエストを書いていく

・今回の記事の取得には

JSONPlaceholder

というAPIを提供しているサービスがあるので利用する。

post/1

https://jsonplaceholder.typicode.com/posts/1

を貼り付け、getリクエストを送る。

・リクエストに成功したケースをthenの中に書く。dispatch関数を呼び、typeにFETCH_SUCCESS、payloadに今受け取ったデータを代入。

次にリクエストに失敗したケースを書く。

・リクエストに失敗した場合catchの中に入ってくるので、catchの中にエラー処理を書く。

・エラー時にはemptyオブジェクトとエラーメッセージを静的に渡しているので今回はtypeのみを渡す。

・最後にjsx内に記事のタイトル、エラーがあった際にはエラーの表示をしていく。

App.js

import React, {useReducer, useEffect} from 'react';
import './App.css';
import axios from 'axios'

const initialState = {
  loading: true,
  error: '',
  post: {}
}


const reducer = (state, action) => {
  switch(action.type) {
      case 'FETCH_SUCCESS':
        return {
          loading: false,
          post: action.payload,
          error: ''
        }
      case 'FETCH_ERROR':
        return{
          loading: false,
          post: {},
          error: 'データ取得をミスりました'
        }
      default:
          return state
  }
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState)

useEffect(() => {
  axios
    .get('https://jsonplaceholder.typicode.com/posts/1')
    .then(res => {
      dispatch({type: 'FETCH_SUCCESS', payload: res.data})
    })
    .catch(err => {
      dispatch({type: 'FETCH_ERROR'})
    })
})

  return (
    <div className="App">
      <h1>{state.loading ? 'Loading...': state.post.title}</h1>
      <h2>{state.error ? state.error: null}</h2>
    </div>
  );
}

export default App;

これで、記事の読み込みができ、取得に失敗した場合はエラーメッセージが出るようになる。

useState vs useReducer

・扱うステートの型

useState:Number, String, Booleanなどのシンプルなプリミティブタイプ

useReducer: Object, Array

・関連するステートを扱う

useState: ×

useReducer: ◎

・ビジネスロジックがある

useState: ×

useReducer: ◎

・ストアーの種類

useState:ローカル

useReducer:グローバル

・テスト

useState: ○

useReducer: ◎

シンプルなステート管理にはuseState、複雑なのにはuseReducerを使うと良い。