nextjs with typescript:40 Reduxの使い方
【0】サンプルアプリ
以下のようなカウントアップアプリを例とする。
このサンプルアプリの作りについて、『親コンポーネントで用意した「変数(state)」と「フック関数」を子コンポーネントへ「バケツリレー」するのではなく、グローバルなstateを用意する方法』でやる。
図に書くと以下のような感じ。
ここではグローバルな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-redux
※3行目のコマンドはtypesync実行 ⇒ yarnでいれてもいい。
【2】Reduxの使い方
ざっくりいうと
①storeをつくって上位コンポーネントとしてラップする
▲このstoreをつくるにあたって、Reducerオブジェクト(処理内容を示すactionとそれに応じた処理を記述したオブジェクト)を作る。
②各コンポーネントで処理を行いたいstoreを指定(useSelector)して、指定の処理をコール(dispatch)する
【3】コード作成:(storeをつくるための)Reducerオブジェクトの作成
繰り返しになるがReduxでグローバルなstateを使うには、storeをつくって、上位コンポーネントとしてラップする。
……これにはcreateStore()に投げ込むReducerオブジェクトが必要となる。
※再掲
まずは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する
それぞれのフックのリファレンス詳細は以下。
画面構成は以下の通り※再掲
【./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オブジェクトの中身を埋め込んで書いた。
その部分を分離して整理する。
↓
【./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
簡単なカウントアップアプリを例にreduxの使い方をまとめた。長くなってきたので、次回、ローカルstateとグローバルstateの回であげたTodo追加アプリの例にreduxを適用してみつつ複数のstateを管理するときについてまとめていく予定
もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。