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

Typescript×ReduxでStateがundefinedに?確認すること!

バイブス父さん
現役の業務SE
2020年5月25日12 min read
Typescript×ReduxでStateがundefinedに?確認すること!

[chatMain]さて今回はTypescriptでReduxを使っていたときのハマりについて書いていこうと思うよ![/chatMain]

[chatReply]プログラミングってハマりが多いっていうもんねー!どんなことではまったの?[/chatReply]

[chatMain]TypesrciptでReduxを使うときに、initialStateというのを使うんだけど、どうしてもこれがundefinedになってしまう!というハマりだよ![/chatMain]

[chatReply]なるほど![/chatReply]

ソースコードを解説

まず説明を始める前に、前提となるソースコードが分からないと話にならないので、早速ソースコードを書いていきます!

このアプリはめちゃくちゃシンプルなカウンタアプリです。

Action

まずはActionでこのアプリでのActionをEnumで定義します。

次にActionCreatorのインターフェースを書いていって、ActionCreatorの実態を書きます。

Action (typescript)#f6e047b97c21
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;
}
 
export const IncrementAmount = () : IIncrement => ({type : ActionNames.Increment});
export const DecrementAmount = () : IDecrement => ({type : ActionNames.Decrement});
 
export type ActionType = IIncrement | IDecrement;
▸ この snippet は実行結果未収録
▸ 実行結果は未収録です

Reducer

続いてReducerでは、上記ActionCreatorに対する処理を書きます。

今回はカウンタアプリなので、シンプルにnumというnumber型の値を持ったオブジェクトをいろいろ変更していきます。

Incrementが来ればプラス1をしますし、Decrementが来ればマイナス1します。

Reducer (typescript)#c6a3f4c3b4fc
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では、後述するContainerで使うためのいろいろな定義を書いていきます。

正直私もここでやってること詳しくはわかりません。

まあReducerが今後増えてくることを想定して、combineReducerを使って、Reducerを一つにする。

また一つにしたReducerをCreateStoreを使って、IndexのProvoderのStoreの引数に投げるということは何となくわかります。

Store (typescript)#3016f3544502
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 = {
	Counter : IState
};
 
export type ReduxAction = ActionType | Action;
▸ この snippet は実行結果未収録
▸ 実行結果は未収録です

Container

っで、Storeでいろいろ作った定義をContainerでいろいろしていきます。

カウンタアプリのコンポーネントの中で使うpropsと紐づける処理をしていて、その中身を把握しているのですが、あっているのでしょうか?

いずれにしても、ここで宣言する関数。IncrementとDecrementをカウンタアプリで使うと思っておけばいいかと思います。

また、connect関数を使って、後述するカウンタアプリのコンポーネントとこのContanerを紐づけます。

Container (typescript)#adc8bd5d5619
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.Counter}
},
	(dispatch : Dispatch<ReduxAction>) => ({actions : new ActionDispather(dispatch)})
)(Counter)
▸ この snippet は実行結果未収録
▸ 実行結果は未収録です

Component

で、カウンタアプリのコンポーネントです。

ここでは、ActionDiapatcherと名付けたクラスが、Propsを通じてくるので、それを使っています。

シンプルにActionDiapatcherで来る値をインターフェースで定義して、受け取る。それを使うって感じですね。

Component (typescript)#cef49ac152ba
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>CurrentCount:{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

INDEXはめっちゃ簡単。

Counterを呼び出して、それをProviderが囲う。

そしてProviderのStoreにstore.tsで作ったstoreを投げる。

注意点はこの時呼ばれるCounterはコンポーネントのCounterではなく、ContainerのCounterということ。indexで気を付けるのはそれくらい。

Index (typescript)#137a571c0501
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 は実行結果未収録
▸ 実行結果は未収録です

Reduxでstateが undefinedに?どんな現象?

っで今回発生しているエラーはこんな内容。

TypeError : Cannot read property ‘num’ of undefined。numとかいうプロパティなんてねえよ!!って話。

エラーの発生源は、CoutnerComponent

なのでrender関数の中身を下記のように変更して、propsの中身をconsole.logで見てみる。

Reduxでstateが undefinedに?どんな現象? (typescript)#724b87f56936
	render() {
		return (
			<div>
				{/*<p>CurrentCount:{this.props.value.num}</p>*/}
				{console.log(this.props)}
				<button type={"button"} onClick={() => this.props.actions.Increment()}>Increment</button>
				<button type={"button"} onClick={() => this.props.actions.Decrement()}>Increment</button>
			</div>
		)
	}
▸ 実行ボタンで結果を表示

すると、actionはしっかりと渡ってきているが、valueでundefinedになってわたってきていないことが判明。

そりゃundefinedのnumを取り出そうとしてもundefinedだわって感じ。

ContainerのMapstateの部分でどのような値が来てるか見てみる。

なので一度Containerのconnect部分でどのようなステータスが渡ってきているが見てみる。

とりあえず、renderのconsoleは削除しないとややこしいから、コメントアウトしておく。

ContainerのMapstateの部分でどのような値が来てるか見てみる。 (typescript)#ddd9708df976
	render() {
		return (
			<div>
				{/*<p>CurrentCount:{this.props.value.num}</p>*/}
				{/*{console.log(this.props)}*/}
				<button type={"button"} onClick={() => this.props.actions.Increment()}>Increment</button>
				<button type={"button"} onClick={() => this.props.actions.Decrement()}>Increment</button>
			</div>
		)
	}
▸ 実行ボタンで結果を表示

っで、Containerのconnectにconsole.logを入れて、stateの中身がどうなっているか見てみる。

ContainerのMapstateの部分でどのような値が来てるか見てみる。 (typescript)#f35cb2945ba1
export default connect(
	(state : ReduxState) => {
		console.log(state);
		return {value : state.Counter}
},
	(dispatch : Dispatch<ReduxAction>) => ({actions : new ActionDispather(dispatch)})
)(Counter)
▸ 実行ボタンで結果を表示

この状態にして実行すると。。。

とりあえずこのタイミングでは、stateの中にnumという値が来ている。

が、注意すべきは、CounterReducerとなっていること。

connectで呼ばれるstateの型は、ReduxStateという型。

この時点で、ReduxStateが臭いとわかる。

ContainerのMapstateの部分でどのような値が来てるか見てみる。 (typescript)#815cfae7aafa
export type ReduxState = {
	Counter : IState
};
▸ この snippet は実行結果未収録
▸ 実行結果は未収録です

見てみると、Counterという名前になっている。でも、Console.logでは、CounterReducerになっていた。

ん?一緒の名前にしないといけない感じ?

とりあえず同じ名前にしてみる。

ContainerのMapstateの部分でどのような値が来てるか見てみる。 (typescript)#6136a1a5f9f4
export type ReduxState = {
	CounterReducer : IState
};
▸ この snippet は実行結果未収録
▸ 実行結果は未収録です

StoreのReduxStateを変更したので、忘れずにReduxStateを使っている箇所の修正をする。

ContainerのMapstateの部分でどのような値が来てるか見てみる。 (typescript)#b91e6363a29b
export default connect(
	(state : ReduxState) => {
		console.log(state);
		return {value : state.CounterReducer}
},
▸ 実行ボタンで結果を表示

あとconponentのコメントアウトも解除しておく

ContainerのMapstateの部分でどのような値が来てるか見てみる。 (typescript)#41a6cb2ed6b9
	render() {
		return (
			<div>
				<p>CurrentCount:{this.props.value.num}</p>
				{console.log(this.props)}
				<button type={"button"} onClick={() => this.props.actions.Increment()}>Increment</button>
				<button type={"button"} onClick={() => this.props.actions.Decrement()}>Increment</button>
			</div>
		)
	}
▸ 実行ボタンで結果を表示

これで実行してみると下記のようになる。

ちゃんと表示されている!

console.logも。。

ちゃんとnumがとれている!

connectのタイミングでundefinedなら?

今回stateがundefinedとなるときに、もし下記のようなエラーの画面だったら?

Reducerでdefaultの値を返していない可能性が高い。

基本的にReduxを使う場合い、ReducerでinisitalStateを定義し、defaultでそれを返すというのがポピュラーになっている。

下記のようにするとバグる。

connectのタイミングでundefinedなら? (typescript)#3c47d076fe68
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;
	}
};
▸ この snippet は実行結果未収録
▸ 実行結果は未収録です

結果ReduxStateの名前をReducerと同じ名前にしないといけない

っで結局わかったのは、connectのStateの型をInterfaceで定義する際に、Reducerの名前を合わせないとバグるという点。

というかTypeScriptはコンパイル時のエラーにはめちゃくちゃ強いけど、実行時エラーは全くわからんって感じ。

だから、console.logとかを使って、デバッグをする技術を磨かんとあかんなーと思った。

今回のファイルはGitに上げてます。

もし今回の挙動を実際に動かしながら見てみたい人は、Gitに上げているのでCloneしてみてください。

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

cloneしてきたフォルダ直下で、yarn init ってコマンド打ってもらえればおそらく必要なライブラリは入るはず。

あとは、yarn startで実行できる。

yarnを入れていない人はこの機会にnpm から npn install yarn でyarnがインストールされるから使ってみるといいと思います。

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

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

運営者について