🌕

React hooks + typescript useReucerの使い方

2022/11/10に公開約7,200字

はじめに

今回は、Reactでは欠かせない便利機能の一つhooksについての紹介です。
Reactjsの記事は、色々情報はありますがtypescriptの記事がなかなかネットで調べても
検索に出てこなくて、非常に困りナレッジとして残す意味も含めて、今回記事にしています。
今回は、以下の記事を参考にさせて頂きました。以下の記事を書いた方は、会社の先輩でいつもお世話になっています。今回もお世話になります。

参考文献

https://zenn.dev/sorye/articles/usereducer-and-usecontext-in-typescript

では、早速本題に移りましょう!

前提知識

ここでは、前提知識について記しています。

  • typescriptの基本的が分かる。(型の書き方や関数の書き方さえ、わかれば十分です。)
  • hooksが少しわかる。(useStateがわかれば大丈夫です。)

useReducer って何?

ここでは、useReducerがどういったものか説明します。
useReducerは、useStateの機能に似ていて、同じく状態を管理することができるhooksです。これらは状態の管理をするという意味では同じものですが、大きな違いがあります。
それは、管理の仕方です。

useStateで場合、例として以下のように宣言します。

const [state, setState] = useState("");

状態変数であるstateの値を書き換える際は、更新用関数setStateで、stateの値を更新します。それに対して、useReducerの場合、以下のように宣言します。

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

形が少しだけ似てますが、なんか違いますね。
ここで、もう抵抗が出る人もいるかもしれません。(自分がそうでした。)

stateの値を書き換えるには、自身で状態を更新するための関数(上記の場合、reducerの中身に処理を記述)を作成し、dispatchで更新用の関数を呼び出すことでstateの更新を行います。多分、「何言ってるんだろう??この人は。」っていう状態だと思います。

実際に使ってみると、なんとなく意味が理解できるとは思います。

実際に、ハンズオン形式で一緒にやってみましょう。
今回は、ユーザーが任意の値で state の数字を、ボタンを押して足す、引くができるものを作成します。完成品はこんな感じです。

コードをコピペすれば、物ができるようにはしますので、一緒に頑張りましょう!

環境構築

好きな場所で、まずはプロジェクトを作成しましょう。以下のコマンドを入力してください。入力後、ファイル名を何にするか聞かれます。ファイル名はご自由にしてください。

npx create-next-app --ts

その他の環境

  • VScode : 1.72.2
  • next : 13.0.2
  • react: 18.2.0
  • typescript : 4.8.4

近ければ、特に問題はないと思います。

フォルダ構成

フォルダの構成は、このようにしています。
Reactを勉強して 2 か月で、フォルダ構成の仕方は、勉強中です。あくまで参考程度にしといてください。

src
    ├─components
    │      main.tsx
    ├─hooks
    │      reducer.ts
    ├─pages
    |      index.tsx
    └─types
           action.ts
           state.ts

コードの記述

まずは、雛形の準備です。
環境ができたら、以下の二つをコピペしてください。

src/components/main.tsx
export const Main = () => {
  return (
    <div
      style={{
        margin: "30px",
        padding: "20px",
        border: "solid",
        width: "600px",
      }}
    >
      <h1>カウント:</h1>
      <div>
        変更値:
        <input
          type="number"
        ></input>
      </div>
      <div
        style={{
          display: "flex",
        }}
      >
        <button>+</button>
        <button>-</button>
      </div>
    </div>
  );
};

export default Main;
src/pages/index.tsx
import type { NextPage } from "next";
import Main from "../components/main";

const Home: NextPage = () => {
  return (
    <>
      <Main />
    </>
  );
};

export default Home;

コピーが終わったら、以下のコマンドで一度サーバーを立ち上げてみましょう。

npm run dev

立ち上げたら、おそらく以下の画面が表示されると思います。

これで雛形が完成しました。
ここから、変更値にユーザーが数字を入力して、「+」を押せばカウントアップして、「-」を押すとカウントダウンする処理を作成します。

処理を作成する前に、どのような状態変数が必要か考えてみましょう。
ここでは、「結果の値を管理するもの」と「変更値を管理するもの」があれば良さそうですね。ということで、上記の 2 つを管理するための状態変数の型を作成しましょう。

src/types/state.ts
export type TState = {
  resultValue: number;
  changeValue: number;
};

これで、管理したい状態変数の型を作成しました。

では、次に更新を行うための関数を作成します。まずは、どのようなアクションを起こしたいか考えてみてください。「カウントアップ」、「カウントダウン」、「任意の値への変更」、これら 3 つがあればできそうですね。ということで、更新関数を作成するための準備をします。いきなり関数を作成せず、型を定義します。ここでは、「関数名」と「関数に渡す引数」を宣言してあげるという感じです。

src/types/action.ts
export type Action =
  | {
      type: "CountUp"; //関数名
    }
  | {
      type: "CountDown"; //関数名
    }
  | {
      type: "ChangeValue"; //関数名
      payload: number; //引数
    };

型定義ができたら、reducerを宣言して、その中に関数を作成します。reducerには、引数として先ほど作成した TState と Action の型で定義した変数を渡します。

src/hooks/reducer.ts
import { Action } from "../types/action";
import { TState } from "../types/state";

export const reducer = (state:TState, action:Action):TState=> {
}

これで、やっと処理の中身を書けます。ただし、最後の戻り値として返すTStateで今エラーが出てると思います.

これはまだState型の戻り値(状態変数)を返してないためエラーが出ています。今は、気にしなくて大丈夫です。では、switch 構文で先ほど宣言した 3 つの関数に対して処理内容を記述していきます。処理を記述すると以下のようになります。ここは、必ず記述していただきたいです!
(型を宣言したことによる、ちょっとした恩恵が得られます。)

src/hooks/reducer.ts
import { Action } from "../types/action";
import { TState } from "../types/state";

export const reducer = (state: TState, action: Action): TState => {
  switch (action.type) {
    case "CountUp":
      return {
        resultValue: state.resultValue + state.changeValue,
        changeValue: state.changeValue,
      };
    case "CountDown":
      return {
        resultValue: state.resultValue - state.changeValue,
        changeValue: state.changeValue,
      };
    case "ChangeValue":
      return { resultValue: state.resultValue, changeValue: action.payload };
  }
};

これで、エラーが出ていた部分が解消されたと思います。先ほどは、TState型を戻り値として返していない為、エラーが出ていましたが、今はreturn値に TState の型に相当するものを返している為、エラーがでてません。もしかすると、TState型を返すのに、辞書型で変数を返していて、よくわからない人がいるかもしれません。そんなときは、戻り値で宣言したTStateにカーソールを合わせてみてください。以下のように、表示されると思います。

よく見ると、returnで返しているものと、TStateの中身が一緒ですね!
「返し方が、わからなくなった!」って人は、戻り値をホバーすると一発で分かります。
ちなみに、スプレッド構文で書く方法もありますが、今回は省略します。気になる方は、調べてみましょう!

ここまで作ったものを、useReducerにまとめていきます。やっと、出てきたかって感じですね。
src/components/main.tsxに移ります。全体を記述すると以下のようになります。

src/components/main.tsx
import { useReducer } from "react";
import { reducer } from "../hooks/reducer";
import { TState } from "../types/state";

//状態の初期値
const initial: TState = {
  resultValue: 0,
  changeValue: 0,
};

export const Main = () => {
  const [rstate, dispatch] = useReducer(reducer, initial);

  const CountUp = () => {
    dispatch({ type: "CountUp" });
  };

  const CountDown = () => {
    dispatch({ type: "CountDown" });
  };

  return (
    <div
      style={{
        margin: "30px",
        padding: "20px",
        border: "solid",
        width: "600px",
      }}
    >
      <h1>カウント:{rstate.resultValue}</h1>
      <div>
        変更値:
        <input
          type="number"
          value={rstate.changeValue}
          onChange={(e) => {
            dispatch({ type: "ChangeValue", payload: +e.target.value });
          }}
        ></input>
      </div>
      <div
        style={{
          display: "flex",
        }}
      >
        <button
          onClick={(e) => {
            e.preventDefault();
            CountUp();
          }}
        >
          +
        </button>
        <button
          onClick={(e) => {
            e.preventDefault();
            CountDown();
          }}
        >
          -
        </button>
      </div>
    </div>
  );
};

export default Main;

コード上で、宣言した useReducer に着目してください。下に書いてるやつですね。

const [rstate, dispatch] = useReducer(reducer, initial);

useReduecerの第一引数は更新用の処理を渡し、第二引数は状態変数の初期値を渡しています。
rstateは現在の状態です。最初の状態は、initialで設定した値になります。
dispatchは、更新用関数で、reducer内の関数を読み取ることで、rstateの値を更新します。

上記のコードのように、ボタンやインプットエリアに更新関数であるdispatchを呼び出せば、今回作成したいものができます。dispatchの引数は最初に宣言したActionの方に沿って書きます。なので、カウントアップだと

dispatch({ type: "CountUp" });

カウントダウンだと

dispatch({ type: "CountDown" });

値の変更だと

dispatch({ type: "ChangeValue", payload: +e.target.value });

このように、なります。呼出し方がわからなくなったら型を確認してみましょう!

最後に

いかがでしょうか。今回が初めての投稿で、至らない点が多々あると思います。まだまだ、始めたばかりで分からない点があります。何か質問やこうした方がもっと良いという意見があれば、よろしくお願いします。では、また次回の記事で!

Discussion

ログインするとコメントできます