この記事は CodeChrysalis Advent Calendar 2019 の記事です。
はじめに
Eliaが書いてくれたstateが配列やオブジェクトの場合の扱い方にインスパイアされて、この記事を書くことにしました。
前提知識としてreduxの知識(store, dispatch, reducer)を理解している前提とします。
Motivation
state を単純化するほうが良いですが、どうしても複雑化する場合もあります。例えばユーザーの情報を登録する場合のフォームを用意している場合、
- 名前
- 名前かな
- 電話番号
- 郵便番号
- 住所
等々の情報を必要とします。これらさえもそれぞれで useState
を使えば単純化できますが、例えばすでにデータベースに登録しているユーザー情報をGETして、それに対して編集を加える場合、GETしたオブジェクトのそれぞれを、それぞれの属性ごとのstateに入れることもできますが、オブジェクトごと変更したい場合は useReducer
が使えます。
コード
Component
import React, { useReducer } from 'react';
import { initFormState, formReducer, ReducerType } from './UserFormReducer';
import { UserFormInterface } from './UserFormInterface';
const Signup: React.FC<{}> = () => {
const [formState, formDispatch] = useReducer<React.Reducer<UserFormInterface, ReducerType>>(
formReducer,
initFormState()
);
...
stateを初期化する関数
export const initFormState = (user?: Partial<UserType>): UserFormInterface => ({
familyName: !user || !user.familyName ? '' : user.familyName,
givenName: !user || !user.givenName ? '' : user.givenName,
familyKana: !user || !user.familyKana ? '' : user.familyKana,
givenKana: !user || !user.givenKana ? '' : user.givenKana,
address1: !user || !user.address1 || user.address1 === '未設定' ? '' : user.address1,
address2: !user || !user.address2 || user.address2 === '未設定' ? '' : user.address2,
email: !user || !user.userEmail ? '' : user.userEmail,
});
stateを初期化する関数のアウトプットの型を表す型
export interface UserFormInterface {
familyName: string;
givenName: string;
familyKana: string;
givenKana: string;
address1: string;
address2: string;
email: string;
}
ReducerのType
export type ReducerType =
| { type: 'familyName'; payload: string | undefined }
| { type: 'givenName'; payload: string | undefined }
| { type: 'familyKana'; payload: string | undefined }
| { type: 'givenKana'; payload: string | undefined }
| { type: 'address1'; payload: string | undefined }
| { type: 'address2'; payload: string | undefined }
| { type: 'email'; payload: string | undefined }
| { type: 'reset' };
Reducer
export const formReducer = (state, action) => {
switch (action.type) {
case 'familyName':
return { ...state, familyName: action.payload };
case 'givenName':
return { ...state, givenName: action.payload };
case 'familyKana':
return { ...state, familyKana: action.payload };
case 'givenKana':
return { ...state, givenKana: action.payload };
case 'address1':
return { ...state, address1: action.payload };
case 'address2':
return { ...state, address2: action.payload };
case 'email':
return { ...state, email: action.payload };
case 'reset':
return initFormState();
default:
return { ...state };
}
};
useReducerの使い方
useReducerのFunction signatureを紹介します。
Input
第一引数:reducer
第二引数:stateを初期化した結果
Output
配列です。useState
と似ていますが、以下の内容となっています。
インデックス0:stateそのもの
インデックス1:dispatcher
そしてもしstateを書き換えたい場合は、dispatcherを使います。上記のコードの例だとformDispatch
です。そしてrenderするときにformState
を使ってたとえば<input>
のvalue
に割り当てることによって、変更した値をレンダリングすることができます。
要素の解説
必要なのは(TypeScriptで書いているということもあって)以下です。
- stateを初期化する関数
- stateを初期化する関数のアウトプットの型を表す型
- reducer
- reducerの型
stateを初期化する関数
再掲です。
export const initFormState = (user?: Partial<UserType>): UserFormInterface => ({
familyName: !user || !user.familyName ? '' : user.familyName,
givenName: !user || !user.givenName ? '' : user.givenName,
familyKana: !user || !user.familyKana ? '' : user.familyKana,
givenKana: !user || !user.givenKana ? '' : user.givenKana,
address1: !user || !user.address1 ? '' : user.address1,
address2: !user || !user.address2 ? '' : user.address2,
email: !user || !user.userEmail ? '' : user.userEmail,
});
ポイントは登録でも更新でも使えるように、引数はuser?: Partial<UserType>
としていて、引数があってもなくても良いようにしています。
ちなみにここではTernary Operatorを使っていますが、最新版のTypescriptであればnullish-coalescingを使うと、もう少しエレガントに書けます。最新版のCreate React Appを使えば使えます。
Outputはユーザーの情報を表すオブジェクトとしています。もし引数にユーザーの情報が引き渡されていればそれを含み、もし含まれていなければ初期化された(何も含まない)オブジェクトとなります。
stateを初期化する関数のアウトプットの型を表す型
再掲です。
export interface UserFormInterface {
familyName: string;
givenName: string;
familyKana: string;
givenKana: string;
address1: string;
address2: string;
email: string;
}
これを定義してあげると、Componentでintellisenseが働いて、Developing experienceが爆上がりします。
reducer
export const formReducer = (state, action) => {
switch (action.type) {
case 'familyName':
return { ...state, familyName: action.payload };
case 'givenName':
return { ...state, givenName: action.payload };
case 'familyKana':
return { ...state, familyKana: action.payload };
case 'givenKana':
return { ...state, givenKana: action.payload };
case 'address1':
return { ...state, address1: action.payload };
case 'address2':
return { ...state, address2: action.payload };
case 'email':
return { ...state, email: action.payload };
case 'reset':
return initFormState();
default:
return { ...state };
}
};
Reduxを理解していれば、そのものですね。state
のオブジェクトをspread operatorを使って新しいオブジェクトに作り直し、さらにreducerに渡された更新内容でアップデートしています。
reducerの型
再掲です。
export type ReducerType =
| { type: 'familyName'; payload: string | undefined }
| { type: 'givenName'; payload: string | undefined }
| { type: 'familyKana'; payload: string | undefined }
| { type: 'givenKana'; payload: string | undefined }
| { type: 'address1'; payload: string | undefined }
| { type: 'address2'; payload: string | undefined }
| { type: 'email'; payload: string | undefined }
| { type: 'reset' };
これも定義してあげると、Componentでintellisenseが働いて、Developing experienceが爆上がりします。
補足ですが、payload
をstring | undefined
としているのは、React上<input>
の初期値はundefined
だからです。
さいごに
useState
とuseReducer
を上手く使いこなして、Reactを楽しく使いましょう!