3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

useReducer を書きながら学ぶ(React Hooks 入門シリーズ 6/6)

Last updated at Posted at 2021-03-14

ロードマップ

React 16.8 で追加された機能であるReactのHooksについて書いてあります。

書きながら学ぶ React Hooks 入門シリーズとして書き下ろしました。

これが最後です。

はじめに

Reactの組み込みフックであるとuseReducerの説明をします。

useReducer とは

  • useStateと同じ様な状態管理用のhook
    • 基本的に、useStateでできることはuseReducerにもできます
  • statedispatch(actionを送信する関数)を返す

構文

const [state, dispatch] = useReducer(reducer, initialState)
  • reducer: state を 更新するための関数
  • dispatch: reducer を 実行するための呼び出し関数
     - dispath には action(何を実行するかをまとめたオブジェクト) を引数を渡す

実践1...カウントアプリ(state 1つ)

useStateでも実装できるカウントアプリを今回はuseReducerを使って実装していきましょう。

手順

  • initialStateの定義
  • reducerの定義
  • useReducerの定義
  • JSXの記述(actionを表示するボタン、カウント数(state)を表示する部分)
サンプルコード
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;
  }
};

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      <p>{state}</p>
      <button onClick={() => {dispatch('increment')}} >+</button>
      <button onClick={() => {dispatch('decrement')}} >-</button>
      <button onClick={() => {dispatch('reset')}} >reset</button>
    </div>
  );
};

export default App;

275b5aeca20974dab556afc12e2e19a7.gif

initialStateが0で、各reducerがちゃんと機能してることがわかります。

実践2...カウントアプリ(複数のstate)

実践1を少し発展させた内容で、複数のstateを持たせていきましょう。

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

これを頭に入れておけば最適な使い分けの一助になるでしょう。

手順

  • initialStateの定義
  • ② JSXにinitialStateの配置
  • ③ dispatch で渡している actionの中にペイロード(アクションの実行に必要な任意のデータ)を含ませる
  • reducerの定義
    • 今回はactionにtypeとvalueをセットしているので、Switch分のcaseの宣言はactionaction.typeに変更
    • secondCounter用にcaseを増やす
    • 複数のstateを含むので、spread構文を用いてprevStateを展開し、更新します
  • useReducerの定義(前と同じ)
  • JSXの記述(actionを表示するボタン、カウント数(state)を表示する部分)
サンプルコード
import React, { useReducer } from "react";

// ① `initialState`の定義
const initialState = {
  firstCounter: 0,
  secondCounter: 10
};

const reducer = (state, action) => {
  // ④ actionにtypeとvalueをセットしているので、Switch分のcaseの宣言は`action`→`action.type`に変更
  switch (action.type) {
    case "increment1":
      return { ...state, firstCounter: state.firstCounter + action.value }; // 複数のstateを含むので、spread構文を用いて`prevState`を展開し、更新します
    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;
  }
};

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      {/* ② JSXにinitialStateの配置 */}
      <p>カウント1: {state.firstCounter}</p>
      <p>カウント2: {state.secondCounter}</p>
    {/* ③ dispatch で渡している actionの中にペイロード(アクションの実行に必要な任意のデータ)を含ませる */}
      {/* ③ action 内を Object にして、typeとvalueをセットする */}
      <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 App;

eca7242260f0b62c9a0b9ed96826c0e5.gif

実践3...useReducer × useContext

useReducer で(ローカルに)扱ってきたStateを useContext を使って (グローバルに)Stateを扱っていきたいと思います。

IMG_7DA18755378F-1.jpeg

今回も前回の実践2と同様カウントアプリを作ります。(グローバルに)Stateにカウント値を持たせて各、子コンポネントで値の参照・更新ができるようにします。

29f613e15e8518fdecc5d72a170823e6.gif

手順

  • 3つの子コンポネント A〜Cの作成
  • 親コンポネントから子コンポネント3つをimport
  • reducerinitialStateの定義
  • useReducerreducerinitialStateを渡すことでcountStateとdispatch関数を作成
  • createContextを使ってcountContextを作成(exportする)
  • countContextを使って Provider を用意して valueには countとdispatchをセットする
  • 各子コンポネントでは
    • 渡されたcountContextをuseContextしてもちいる
    • valueからcountを表示させとdispatch関数からincrement, decrementを使えるようにする
サンプルコード
App.js
import React, { useReducer, createContext, createElement } from "react";
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;
  }
};

const App = () => {
  const [count, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      <h1>カウント{count.firstCounter}</h1>
      <CountContext.Provider
        value={{ countState: count, countDispatch: dispatch }}
      >
        <ComponentA />
        <ComponentB />
        <ComponentC />
      </CountContext.Provider>
    </div>
  );
};

export default App;
components/ComponentA
import React, { useContext } from "react";
import { CountContext } from "../App";

export const ComponentA = () => {
  const countContext = useContext(CountContext);
  return (
    <div>
      <p>ComponentA</p>
      <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>
  );
};
components/ComponentB
import React, { useContext } from "react";
import { CountContext } from "../App";

export const ComponentB = () => {
  const countContext = useContext(CountContext);
  return (
    <div>
      <p>ComponentB</p>
      <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>
  );
};
components/ComponentC
import React, { useContext } from "react";
import { CountContext } from "../App";

export const ComponentC = () => {
  const countContext = useContext(CountContext);
  return (
    <div>
      <p>ComponentC</p>
      <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>
  );
};

実行結果

29f613e15e8518fdecc5d72a170823e6.gif

それぞれの子コンポネントの動作が確認できました。こうすることで、useReduceruseContextを使ってグローバルにstateを管理することができます。

実践4...useReducer × 非同期処理(useEffect)

useReduceruseEffectを使って非同期処理させて外部APIからデータ(記事の擬似データ)を取得・表示させてみましょう。

b2e512d2cb1ee20debac0bb334563ba6.gif

手順

  • useReduceruseEffectのimport
  • 非同期処理はaxiosを使う
  • initialStateの定義(loading, error, post)
  • reducerの作成
    • 引数(state,action
    • return 新しいstate
    • switch ケースは HTTPリクエストの成功時(FETCH_SUCCESS) or 失敗時(FETCH_ERROR)で処理を分けます
  • useReducerinitialStatereducer関数を読み込ませて、statedispatch関数を利用できるようにします
  • useEffect内にHTTPリクエスト非同期処理を記載する
  • JSXに、FETCH_SUCCESS時とFETCH_ERROR時の場合のコンポーネントを書く
サンプルコード
App.js
import React, { useEffect, useReducer } from "react";
import axios from "axios";

const initialState = {
  loading: true,
  error: "",
  post: {}
};

const reducer = (state, action) => {
  switch (action.type) {
    case "FETCH_SUCCESS":
      return {
        loading: false,
        error: "",
        post: action.payload
      };
    case "FETCH_ERROR":
      return {
        loading: false,
        error: "データ取得に失敗",
        post: {}
      };
    default:
      return state;
  }
};

const 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(() => {
        dispatch({ type: "FETCH_ERROR" });
      });
  }, []);

  return (
    <div>
      <h1>{state.loading ? "Loading..." : state.post.title}</h1>
      <p>{state.error ? state.error : null}</p>
    </div>
  );
};

export default App;

実行結果

b2e512d2cb1ee20debac0bb334563ba6.gif

非同期処理なので、時間差で 「Loading」がtitleに変わっていることがわかります。

シーン別 useState と useReducer 使い分け

結論、シンプルなstateにはuseStateが向いていて、複雑なstate管理にはuseReducerがいいでしょう。

表にして、特徴をまとめました。

useState useReducer
扱うといい state の型 Number, String, Bool などのプリミティブ型を更新する時 Array, Object の更新に使う時
どんなシーンで使うといい? 単独のstate(複数でもできるっちゃできる) 複数のstateを同時に更新したい時
stateの更新に複雑なビジネスロジックがある場合 好ましくない 好ましい
ストアーの種類 ローカルな管理に向いてる useContextと合わせてグローバルに管理するとよい

以上です!これで最後です。

連載の最後に

シリーズ全部読んでくれた方へ。

どうもありがとうございました。

参考

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?