見出し画像

nextjs with typescript:40 Reduxの使い方

【0】サンプルアプリ

以下のようなカウントアップアプリを例とする。

画像1

 このサンプルアプリの作りについて、『親コンポーネントで用意した「変数(state)」と「フック関数」を子コンポーネントへ「バケツリレー」するのではなく、グローバルなstateを用意する方法』でやる。
 図に書くと以下のような感じ。

画像2

 ここではグローバルなstateの取り扱いにReduxを使う(RTK:Redux Tool Kitは別途説明予定)

【1】Redux, React Reduxのインストール

必要なライブラリは「Redux」と「React Redux」の2つ。(※Redux自体はReact専用のものではない。ReactでもReduxが使えるようするにはReact Reduxが必要)

yarn add redux
yarn add react-redux
yarn add --dev @types/react-redux3行目のコマンドはtypesync実行 ⇒ yarnでいれてもいい。

【2】Reduxの使い方

ざっくりいうと
①storeをつくって上位コンポーネントとしてラップする

画像3

▲このstoreをつくるにあたって、Reducerオブジェクト(処理内容を示すactionとそれに応じた処理を記述したオブジェクト)を作る。

②各コンポーネントで処理を行いたいstoreを指定(useSelector)して、指定の処理をコール(dispatch)する

画像4

画像5

【3】コード作成:(storeをつくるための)Reducerオブジェクトの作成

繰り返しになるがReduxでグローバルなstateを使うには、storeをつくって、上位コンポーネントとしてラップする。
……これにはcreateStore()に投げ込むReducerオブジェクトが必要となる。
※再掲

画像6

まずはReducerオブジェクトを作成する。
「Reducerオブジェクト」は簡単に言うと、引数にstateとactionの2つの引数を持ち、その2つを引数も使うなどしてstoreに与える新たなstateを返すオブジェクト。という感じ。


【./duck/MyButton/countReducer.ts】
※Reduxの利用する際、フォルダやファイルのおき方にはいくつか定番のパターンはあるのだが、今回は気にしないで適当。→duckフォルダを作ってその配下に置くことにする。

//Reducerが受け付けるstateの型
export interface CountState{
   counter:number;
};

//初期値
const initalState = {
   counter:0
}

//Action型の構造(エイリアス)
type Action = { 
   type:"count/increment", 
   payload:number
};

//Reducerオブジェクト本体
export const countReducer = (state:CountState=initalState,action:Action) => {
   
   //ここにswitch文でaction分岐
   switch(action.type){
       case "count/increment":
           //新しいstate(CountState)オブジェクトを返す
           return { counter:state.counter + action.payload }
       default:
           //既存のstateを返す
           return state;

   }
}

【4】コード作成:storeをつくって上位コンポーネントとしてラップ

storeは「createStore()」で作成する。
【./store.ts】
※_app.tsx内に書いてもいいが今回は別ファイルにexport定義

import { countReducer } from './duck/MyButton/countReducer';
import { createStore } from 'redux';

export const store = createStore(countReducer);

↓ これを使って「_app.tsx」でコンポーネントをラップする

【./pages/_app.tsx】
※_app.tsxはMaterial-UIの回のものを流用。エラーになっている部分を最低限修正しただけで余計な記述もある。

import React from 'react';
import Head from 'next/head';
import { AppProps } from 'next/app';


import {Provider} from 'react-redux';
import {store} from '../store';

export default function MyApp(props: AppProps) {
 const { Component, pageProps } = props;

 React.useEffect(() => {
   // Remove the server-side injected CSS.
   const jssStyles = document.querySelector('#jss-server-side');
   if (jssStyles) {
     jssStyles.parentElement!.removeChild(jssStyles);
   }
 }, []);

 return (
   <React.Fragment>
     <Head>
       <title>My page</title>
       <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
     </Head>

       <Provider store={store}>
           <Component {...pageProps} />
       </Provider>
   </React.Fragment>
 );
}

これでグローバルなstateを使用する準備ができた。

【5】各コンポーネントでグローバルなstateを参照(useSelector)しdispatchする

それぞれのフックのリファレンス詳細は以下。

画面構成は以下の通り※再掲

画像7

【./components/MyButton.tsx】

import { useDispatch, useSelector } from "react-redux";
//import {CountState} from "./countReducer"; //掲載ミスのためコメントアウト。
import {CountState} from "../duck/MyButton/countReducer";

const MyButton = () =>{

   //第一引数がstateの型
   //第二引数CountState["counter"]はuseSelectorが返す型相当 つまり、今回はnumber
   const mycounter = useSelector<CountState,CountState["counter"]>((state)=>state.counter);
   const dispatch = useDispatch();


   const handleCountUpBtn = (value:number) => {
       //dispatchにactionオブジェクト(今回はtypeとpayload)を渡してstoreで処理させる
       dispatch({type:"count/increment", payload:value});
   }

   return(
       <div>
               <button onClick={()=>handleCountUpBtn(1)}> [+] <MyButton />  </button>
               ※debug counter:{mycounter}
       </div>
   );
}

export default MyButton


【./pages/home.tsx】

import { useDispatch, useSelector } from "react-redux";

import {CountState} from "../duck/MyButton/countReducer";
//import {CountState} from "../components/countReducer"; //掲載ミスのためコメントアウト。
import MyButton from "../components/MyButton";


const Home = () =>{

   //第一引数がstateの型
   //第二引数CountState["counter"]はuseSelectorが返す型相当 つまり、今回はnumber
   const counter = useSelector<CountState,CountState["counter"]>((state)=>state.counter);
   const dispatch = useDispatch();

   const handleCountUpBtn = (value:number) => {
       dispatch({type:"count/increment", payload:value});
   }

   return(
       <div>
           counter:{counter}
           <div>
               <button onClick={()=>handleCountUpBtn(1)}> [+] (home.tsx)  </button>
               <hr/>
               <MyButton />
           </div>
       </div>
   );
}

export default Home

【6】Action部分を分離する

「./pages/home.tsx」や「./components/MyButton.tsx」において「dispatch()」関数内にActionオブジェクトの中身を埋め込んで書いた。
その部分を分離して整理する。

画像8


【./duck/MyButton/actions.ts】

//Action型のエイリアス
export type Action = { 
   type:"count/increment", 
   payload:number
};


export const countIncrement = (value:number):Action => {
   return(
       {
           type:"count/increment",
           payload:value
       }
   );
}

▲結局はdispatch()内に直接うめこんでいた部分を関数コールにした、って感じ。

↓ dispatchをコールする側も修正
【./pages/home.tsx】

import { useDispatch, useSelector } from "react-redux";

import {CountState} from "../duck/MyButton/countReducer";
import MyButton from "../components/MyButton";
import { countIncrement } from "../duck/MyButton/actions"; //action部分をインポート


const Home = () =>{

   const counter = useSelector<CountState,CountState["counter"]>((state)=>state.counter);
   const dispatch = useDispatch();

   const handleCountUpBtn = (value:number) => {
       //dispatch({type:"count/increment", payload:value});
       dispatch(countIncrement(value));
   }

   return(
       <div>
           counter:{counter}
           <div>
               <button onClick={()=>handleCountUpBtn(1)}> [+] (home.tsx)  </button>
               <hr/>
               <MyButton />
           </div>
       </div>
   );
}

export default Home

【./components/MyButton.tsx】※home.tsx同様

import { useDispatch, useSelector } from "react-redux";
import { countIncrement } from "../duck/MyButton/actions";
import {CountState} from "../duck/MyButton/countReducer";

const MyButton = () =>{

   const mycounter = useSelector<CountState,CountState["counter"]>((state)=>state.counter);
   const dispatch = useDispatch();


   const handleCountUpBtn = (value:number) => {
       //dispatch({type:"count/increment", payload:value});
       dispatch(countIncrement(value));
   }

   return(
       <div>
               <button onClick={()=>handleCountUpBtn(1)}> [+] <MyButton />  </button>
               ※debug counter:{mycounter}
       </div>
   );
}

export default MyButton

画像9

簡単なカウントアップアプリを例にreduxの使い方をまとめた。長くなってきたので、次回、ローカルstateとグローバルstateの回であげたTodo追加アプリの例にreduxを適用してみつつ複数のstateを管理するときについてまとめていく予定

もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。