👻

useState禁止令、useReducerを使え

2023/01/31に公開
2

はじめに

修正や追加等はコメントまたはGitHubで編集リクエストをお待ちしております。

本題

useState便利ですよね?
Reactの状態管理といったらこれを思い浮かべると思います。
ですが状態が増えたらどうしますか?
複数の状態を更新するhookを作るときにsetStateを引数にいくつも取る?
Objectを持ったstateを更新するときに、Objectの中身を全部書く?
気がついたら技術負債の完成です。
useReducerを使いましょう。

useReducerの使い方

第一引数にreducer関数を渡します。
reducer関数は、第一引数にstate、第二引数に任意のオブジェクトを受け取り、stateを更新します。
利用する際は第一引数は省略します。

一番シンプルな例

interface State {
 name: string;
 email: string;
}

const [state, dispatch] = useReducer(
  (
   state: State,
   newState: Partial<State>,
  ) => ({ ...state, ...newState }),
  { name: "", email: "" }
);

dispatch({ name: "hoge" });

簡単なバリデーション

interface State {
 name: string;
 email: string;
}

const [state, dispatch] = useReducer(
  (
   state: State,
   newState: Partial<State>,
  ) => {
   if (newState.name && newState.name.length > 10) {
    return { ...state, ...newState };
   }
   return { ...state };
  },
  { name: "", email: "" }
);

Redux風

interface State {
 name: string;
 email: string;
}

const [state, dispatch] = useReducer(
  (
   state: State,
   action: {
    type: "UPDATE_NAME" | "UPDATE_EMAIL";
    payload: string;
   },
  ) => {
   switch (action.type) {
    case "UPDATE_NAME":
     return { ...state, name: action.payload };
    case "UPDATE_EMAIL":
     return { ...state, email: action.payload };
    default:
     return state;
   }
  },
  { name: "", email: "" }
);

dispatch({ type: "UPDATE_NAME", payload: "hoge" });

useReducerのメリット

useReducerを使うことで、1つのstateで複数の状態を管理できます。
一部の状態だけ更新したい場合は、reducer関数で更新したい状態だけ更新するようにします。
また、reducer関数はuseReducerの外に書くことができます。
これにより、コンポーネントの見通しが良くなります。
複数コンポーネントでreducer関数を共有できます。

interface State {
 name: string;
 email: string;
}
const initialState: State = { name: "", email: "" };
export const reducer = (
 state: State,
 action: {
  type: "UPDATE_NAME" | "UPDATE_EMAIL" | "RESET";
  payload: string;
 },
) => {
 switch (action.type) {
  case "UPDATE_NAME":
   return { ...state, name: action.payload };
  case "UPDATE_EMAIL":
   return { ...state, email: action.payload };
  case "RESET":
   return initialState;
  default:
   return state;
 }
};

const [state, dispatch] = useReducer(reducer, initialState);

RecoilとかReduxとはどうやって使い分けるの?

コンポーネントを跨ぐ場合useReducerはバケツリレーをしないといけません。
コンポーネント間で共有する情報はRecoil(ユーザーのログイン情報など)
コンポーネントだけの情報はuseReducer(お問い合わせフォームの入力内容など)

まとめ

useReducerで技術負債を減らしましょう。

GitHubで編集を提案

Discussion

Naoki FujitaNaoki Fujita

この例だと

const [name, setName] = useState('');
const [email, setEmail] = useState('');

の方が誰にでも理解でき、端的なのではないかと思いました。

  1. 沢山のsetStateのバケツリレーをしたくない→stateをオブジェクトとして持つ
  2. stateをオブジェクトにしたとき、useStateはあまりにも自由すぎる→useReducerでリアクティブ変数のとりうる状態空間を狭める

という論法であれば概ね同意できます