🏌️‍♂️

初めてのReact & Redux

2020/12/24に公開

はじめに

reduxについて勉強し、かなり簡単なプロジェクトですが作成してみました!

また React を使った成果物は今までcreate-react-appにお世話になっていましたが、今回はwebpackで環境構築をしております。またwebpackについても勉強して記事にできたらなと考えております。
githubにて公開していますので、ぜひご覧ください。

プロジェクトの概要

プロジェクトは「投票箱」と称して、投票数の追加や削減、また個人の投票数と全員の投票数を確認できるといった機能です。

ファイル構成

├── src/
   ├── components/
           └──Counter.module.css  
	   └──Counter.tsx
   └──action.ts
   └──App.module.css
   └──App.tsx
   └──index.html
   └──index.tsx
   └──reducer.ts

トップレベルであるindex.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { counterReducer, initialState } from './reducer';

const store = createStore(counterReducer, initialState);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root') as HTMLElement
);

Reduxを使用するにあたって、props として store を渡してあげた Provider というコンポーネントでAppコンポーネントをラップする必要があります。
store は createStore という関数で作成し、初期化する必要があります。またこの関数の引数にはreducer と state の初期値を渡す必要があります。後ほど reducer.ts ファイルを確認する中でご紹介します。

storeの状態(state)を変更するためのAction

action.ts
export const ActionType = {
  DECREMENT: 'DECREMENT',
  INCREMENT: 'INCREMENT',
} as const;

type ValueOf<T> = T[keyof T];

export type Action = {
  type: ValueOf<typeof ActionType>;
  amount?: number;
};

export const decrement = (): Action => ({
  type: ActionType.DECREMENT,
});
export const increment = (): Action => ({
  type: ActionType.INCREMENT,
});

action の役割としてはイベントの種類によって区別するためのオブジェクトを作り出すことです。
今回、必要なイベントはインクリメント、デクリメントのに種類なので ActionType でそれを定義しています。

actionで作り出すオブジェクトでは、イベントの種類を格納するプロパティ type と、加算する数値を格納するプロパティの amount が必要であるので type Action でオブジェクトの型定義を行っています。
プロパティー type にマウスホバーすると以下のようなインクリメントとデクリメントによるユニオン型になります。

そしてaction.tsの最後にはactionを生成するための関数を定義しています。この関数によってstoreの状態を変更します。

新しい状態(state)を生成するreducer

reducer.ts
import { Reducer } from 'redux';
import { Action, ActionType as Type } from './action';

export type CounterState = {
  count: number;
};
export const initialState: CounterState = { count: 0 };

export const counterReducer: Reducer<CounterState, Action> = (
  state: CounterState = initialState,
  action: Action
): CounterState => {
  switch (action.type) {
    case Type.DECREMENT:
      return {
        ...state,
        count: state.count - 1,
      };
    case Type.INCREMENT:
      return {
        ...state,
        count: state.count + 1,
      };

    default:
      const check: never = action.type;//action.typeによるcase文の漏れを未然にチェック
      return state;
  }
};

reducerの役割としては、現在の状態とactionを受け取って、新しい状態を作り出すことです。

まず、状態の型、状態の初期値をそれぞれ定義しています。
そしてcounterReducerでは現在の状態とactionを引数に割り当てて、actionで定義したイベントの種類によって新しい状態を返す関数を表しています。
先程、index.tsxでcreateStoreという関数の引数に渡していたreducerとstateの初期値がここで確認できました。

ちなみにcounterReducerでジェネリック型を用いて型定義していいるReducerにマウスカーソルを当てF12を押すと index.d.ts にて型定義が確認できます。index.d.ts

コンポーネント側でReduxの機能を実装

App.tsx
import React from 'react';
import { useSelector } from 'react-redux';
import { CounterState } from './reducer';
import Counter from './components/Counter';
import AppCSS from './App.module.css';

type VoteManga = string[];

const App = () => {
  const count = useSelector<CounterState, number>((state) => state.count);
  const votesManga: VoteManga = ['Aさん', 'Bさん', 'Cさん'];

  return (
    <div className={AppCSS.container}>
      <div className={AppCSS.header}>
        <h1>投票箱</h1>
        <h2>Total Votes:{count}</h2>
      </div>
      {votesManga.map((vote, i) => (
        <Counter key={i} name={vote} />
      ))}
    </div>
  );
};

export default App;

count 変数の定義でRedux HooksであるuseSelectorを使用しています。useSelector によってstoreの状態(state)が取得できます。ここでは全員の投票数を確認できる機能を実装しています。

今回は reducer で定義した state の種類が count だけでしたが、大規模プロジェクトになればこの state の種類が複数あり、各コンポーネントで必要な store の state を useSlector によって取得するイメージでしょうか。

Counter.tsx
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { increment, decrement } from '../action';
import CounterCSS from './Counter.module.css';

interface Props {
  name: string;
}
const Counter: React.FC<Props> = ({ name }) => {
  const dispatch = useDispatch();
  const [votes, setVotes] = useState<number>(0);

  const handleDecrement = () => {
    dispatch(decrement());
    setVotes(votes - 1);
  };
  const handleIncrement = () => {
    dispatch(increment());
    setVotes(votes + 1);
  };

  return (
    <div className={CounterCSS.header}>
      <h2>{name}</h2>
      <h3>{`Votes: ${votes}`}</h3>
      <div>
        <button onClick={handleIncrement}>Increment</button>
        <button onClick={handleDecrement}>Decrement</button>
      </div>
    </div>
  );
};

export default Counter;

dispatch 変数の定義でRedux Hooksである useDispatch を使用しています。この dispatch 変数にactionを引数に渡すことで、各イベントの action が dispatch されるようになっています。ここでは個人の投票数の確認、追加、削減する機能を実装しています。

以上になります。ここまで読んでいただきありがとうございました!!

Discussion