動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

Typescript×Reduxでカウンタアプリ作ってみる!

バイブス父さん
現役の業務SE
2020年5月23日9 min read
Typescript×Reduxでカウンタアプリ作ってみる!

[chatMain]みなさんこんにちは!今回はTypescriptとReduxでカウンタアプリを作ってみようと思います![/chatMain]

[chatReply]おお!いつもは商品レビューとか書いてるのに、今日はエンジニアっぽいこと書くんだな![/chatReply]

[chatMain]もちろん!そもそも私はエンジニアですからね!たまにはエンジニアっぽいことも書かないとなーと思って筆を取った次第です![/chatMain]

今回の成果物

ではまず最初に今回のアプリの挙動を見ていきたい。

 

各ソースコード

今回のアプリで必要なライブラリは下記の通り!

  • react-redux
  • redux
  • @types/react-redux
  • @types/redux

最低限これらがあれば動作するようになっている。

初期状態では、カウントは0になっている。

incrementボタンを押すと1カウントがプラスされる。

decrementを押すと1カウントがマイナスされる。

 

超絶シンプルなカウンタアプリになっている。

 

Action

まずEnumで今回のアプリで実装するActionを定義する。

incrementとdecrementだけだから、そのまま定義。

次に、InterfaceでActionの型を定義する。

今回はreduxライブラリのActionを継承したInterfaceを作っているが、Actionはtypeを持っているらしい。

 

最後にDispacherで使うためのAmountを実装する。

Action (sql)#471a1378c67a
import {Action} from 'redux'



export enum    ActionNames{

   Increment = 'inc',

   Decrement = 'dec'

}



export interface IIncrement extends Action{

   type : ActionNames.Increment

}



export interface IDecrement extends Action {

   type : ActionNames.Decrement;

}



// 名前は何でもいい。IncrementでもIncでも。なんでも。

// 戻り値は上で定義したInterfaceを返すようにする。

// いわばActionCreatorといわれるやつ。

export const IncrementAmount = () : IIncrement => ({type : ActionNames.Increment});

export const DecrementAmount = () : IDecrement => ({type : ActionNames.Decrement});



export type ActionType = IIncrement | IDecrement;

▸ この snippet は実行結果未収録
▸ 実行結果は未収録です

Reducer

続いてReducerではactionに対する実装を書く。

また同時に画面でどのようなstateを持っているのかも書く。

今回の場合は、数字を増やしたりするだけなので、num一つだけ持っている。

またreducerの中のreturnで結構ミスをして、stateが変わらない!ってケースが多いから注意。

Reducer (sql)#4d8b83057790
import {ActionNames, ActionType} from "../Actions/CounterAction";





export interface IState {

   num : number

}



const initialState : IState = {num: 0};



const CounterReducer = (state : IState = initialState , action : ActionType): IState => {

   switch (action.type) {

      case ActionNames.Increment:

         // 値が変わらない場合などはここでミスってる場合が多いから注意。 

         return {num : state.num + 1};

      case ActionNames.Decrement:

         return {num : state.num - 1};

      default:

         return state;

   }

};



export default CounterReducer;
▸ この snippet は実行結果未収録
▸ 実行結果は未収録です

Store

storeはよくわからん。

今時点では、私も書き方を理解してないため、呪文のようにただただ覚えて書いている。

ただ、後ほど出てくるIndex.tsxのProviderコンポーネントのstoreに渡すためのstoreのオブジェクトを作るとは知っている。

私は今の時点では、こんなもんかーって感じ。

Store (sql)#c018959893e8
import {combineReducers,createStore,Action} from 'redux'

import CounterReducer, {IState} from "../Reducers/CounterReducer";

import {ActionType} from "../Actions/CounterAction";



const rootReducer = combineReducers(

   {CounterReducer}

   );



export default createStore(rootReducer);



export type ReduxState = {

   // ここでReducerの名前を正しく書かないとstateがバグる 

   CounterReducer : IState

};



export type ReduxAction = ActionType | Action;
▸ この snippet は実行結果未収録
▸ 実行結果は未収録です

Container

コンポーネントの中で実装する関数を定義する?場所的な感じだと思う。

ここで定義した関数をコンポーネントで使えるようになる。

また、connectの中でいろいろしてるが、これも今は呪文的な感じで書いている。

コンポーネント内で使うpropsと紐づけるためにいろいろ書いている。と理解している。

Container (sql)#22309cd4a543
import {ReduxAction, ReduxState} from "../Store/store";

import {DecrementAmount, IncrementAmount} from "../Actions/CounterAction";

import {connect} from "react-redux";

import {Dispatch} from "react";

import Counter from "./Counter";



export class ActionDispather{

   constructor(private dispatch: (action : ReduxAction) => void) {

   }



   public Increment() {

      this.dispatch(IncrementAmount())

   }



   public Decrement(){

      this.dispatch(DecrementAmount())

   }

}



export default connect(

   (state : ReduxState) => {

      return {value : state.CounterReducer}

},

   (dispatch : Dispatch<ReduxAction>) => ({actions : new ActionDispather(dispatch)})

)(Counter)

▸ この snippet は実行結果未収録
▸ 実行結果は未収録です

ちなみに下記の箇所は書き方がいっぱいある。

export class ActionDispatcher {
   constructor(private dispatch: (action: ReduxAction) => void) {}

パターン1

export class ActionDispatcher {
   dispatch : (action: ReduxAction) => void
   constructor(dispatch: (action: ReduxAction) => void) {
      this.dispatch = dispatch;
   }

パターン2

type dispatchType = (action: ReduxAction) => void

export class ActionDispatcher {
   dispatch : dispatchType;
   constructor(dispatch: dispatchType) {
      this.dispatch = dispatch;
   }

パターン3

type dispatchType = (action: ReduxAction) => void

export class ActionDispatcher {
   constructor(private dispatch: dispatchType) {
   }

なにをしているのか?

これは結局のところ型付けである。

actionという名前で、ReduxAction型を受け取り、戻り値がvoid型の関数という型を指定しているのである。

なので、この関数の型をtypeエイリアスに入れてしまえば結構楽になる。

Component

続いて、コンポーネント。

好きかって書いていいと思う。

が、Propsは上記のContainerのconnectで書いた型を受け付けるようInterfaceで宣言にしないといけない。

もちろんコンポーネントが上記で指定した型のpropsを受け取れるようにする。

Component (sql)#038f1a12561a
import React from "react";

import {IState} from "../Reducers/CounterReducer";

import {ActionDispather} from "./Container";



interface Props {

   value : IState;

   actions : ActionDispather

}





class Counter extends React.Component<Props, {}>{



   render() {

      return (

         <div>

            <p>{this.props.value.num}</p>

            <button type={"button"} onClick={() => this.props.actions.Increment()}>Increment</button>

            <button type={"button"} onClick={() => this.props.actions.Decrement()}>Increment</button>

         </div>

      )

   }

}



export default Counter
▸ この snippet は実行結果未収録
▸ 実行結果は未収録です

Index.tsx

Indexはシンプル。

Providerで囲んで、さっき作ったstoreを渡すだけ。

どういう仕組みかは詳しく知らんが、これで動く。

Index.tsx (sql)#e82df7884099
import React from 'react';

import ReactDOM from 'react-dom';

import * as serviceWorker from './serviceWorker';

import {Provider} from "react-redux";

import store from "./Store/store";

import Counter from "./Component/Container";



ReactDOM.render(

  <Provider store={store}>

    <Counter />

  </Provider>,

  document.getElementById('root')

);



// If you want your app to work offline and load faster, you can change

// unregister() to register() below. Note this comes with some pitfalls.

// Learn more about service workers: https://bit.ly/CRA-PWA

serviceWorker.unregister();

▸ この snippet は実行結果未収録
▸ 実行結果は未収録です

Gitに上げてます!

このコードはGitのリポジトリに上げています。

私はTypescript×Reduxを学ぶときに、まずは動くアプリを目標にしました。

なぜならとりあえず動けば、いろいろなステータスを変えればどうなっているかわかるから。

ただネット上にはコンパイルエラーだの何かのエラーで動くコードがあるというのがまれで、結構な時間を消費した。

だから今回のアプリは実際に動く形としてGitに上げている。

  • https://github.com/adaman3568/Typescriot-CounterApp/network/dependencies.git

以上。

 

この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について