LoginSignup
3
4

More than 3 years have passed since last update.

React の useReducer を活用して state が複雑化するのを避ける

Posted at

この記事は 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が爆上がりします。

補足ですが、payloadstring | undefinedとしているのは、React上<input>の初期値はundefinedだからです。

さいごに

useStateuseReducerを上手く使いこなして、Reactを楽しく使いましょう!

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4