LoginSignup
1
0

More than 3 years have passed since last update.

2. State 管理を Redux に移行する - Redux よくわからんので Todo つくる

Last updated at Posted at 2020-05-25

はじめに

前回は、React のみでこのようななんでもない Todo アプリを作成しました
react-todo.gif

はい
なんでもないですね

今回は当初の目的である、State 管理の Redux への移行を行います

今回からいろいろと Rollup に惨敗して Webpack 使ってます
なんにも知らないくせに使うから...

前回のコードに変更はないので、それだけ

なぜ Redux を使うのか?

「なぜ State 管理を分離するのか?」と言ったほうが正しいかも

今回のような、小規模も小規模なアプリではほぼ無縁な問題ですが、アプリの規模が大きくなると、以下のような様々な問題が浮き彫りとなりやがります

  • State 分散されすぎ... いちいち別ファイル開くのが面倒、てかどこ? 😥
    • (トップコンポーネントに State 集中しすぎることのほうが多そう
  • State 管理用のコードがコンポーネント内に増えまくり... お手上げ 🙋‍♂️
  • UI、State 処理のコードが混在して、コードが読みづらい... 🍝
  • Prop のバケツリレーつらい 🥺
  • etc...

しかし、Redux を使うと...?

  • State は一点管理✨ どこにあるかも一目瞭然!! 😃
  • State 管理のコードはコンポーネント外部に追いやり! State 管理は Redux に任せとき~~ 😘
  • コンポーネントは描画に集中! 内部には UI 処理のコードだけ!! 🥰
  • 必要なコンポーネントが直接 State 変更関数を受け取り!! 😍
  • etc...

YEAH 😎

React 単体でもある程度マシに出来ると思いますが、そもそも React は UI 構築ライブラリであることを意識しておいたほうが良いんじゃないかなあ~と思います
(結局バケツリレーはどうにもなりませんし)

あくまで React の言う State というのは "UI(表示) に関するもの" のことであって "表示されるデータ" では無い...的な(わかって)
そこをはっきりとさせることで、開発の効率は確実に上がると考えます
(バケツリレーも無くなりますし)

コーディングの基本は分割...ってね

ディレクトリ構造

src/
 ├ components/
 │  └ app/
 ├ actions/
 ├ store/
 │  └ reducer/
 └ index.ts - エントリ

まず、Redux 用のフォルダが追加されるため、前回 src/ 直下にあった App コンポーネントフォルダを components/ に移動しています

そして、Redux 用に actions/, store/, store/reducer/ を作成します

パッケージの追加

yarn add redux react-redux
yarn add -D @types/react-redux

Store の作成

まずは Store を作らないことには React との連携もへったくれも無いので Redux 単体の機能である Store を作成します

必要な要素

Action
コンポーネントが、Store に対して「何が起きたか」を説明する
store.dispatch() を使用して Store に送信する

Action は以下のような type プロパティを持つただのオブジェクト

{
  type: "Reducer が Action の種類を識別するための文字列",
  // あとは自由
  // State 変更に必要なデータを入れておく
}

Action Creator
コンポーネントからデータを受け取り、Action を作成する

Reducer
Store に送信された Action を受け取り、State がどのように変化するかを指定する
事実上実際 State の変更を担当する大事なトコ

Store
アプリケーションに一つだけ存在し、State を保持する

  • State へのアクセス手段 (store.getState())
  • State の更新手段 (store.dispatch())
  • 更新リスナの登録 (store.subscribe())

を提供する

Action Creator

誤字とかでエラーが出るのを防ぐために Action type を別に定義しておきます
Action Creator に渡した時に string 型になるのを防ぐために as const を付けています

src/actions/todo/index.ts
const ADD_TODO = "ADD_TODO" as const;
const TOGGLE_COMPLETED = "TOGGLE_COMPLETED" as const;
const DELETE_TODO = "DELETE_TODO" as const;
src/actions/filter/index.ts
const SET_FILTER = "SET_FILTER" as const;

あとは必要なデータを引数に受け取り、いい感じに加工して Action を作ります
若干 JSON API 意識で、必要なデータは全て data プロパティ内に入れています

src/actions/todo/index.ts
const addTodo = (text: string) => ({
  type: ADD_TODO,
  data: { id: Math.random(), text, complete: false },
});

const toggleCompleted = (id: number, isCompleted: boolean) => ({
  type: TOGGLE_COMPLETED,
  data: { id, isCompleted },
});

const deleteTodo = (id: number) => ({
  type: DELETE_TODO,
  data: id,
});
src/actions/filter/index.ts
const setFilter = (filter: FilterStateType) => ({
  type: SET_FILTER,
  data: filter,
});

型定義

Reducer で型チェックするため、ReturnType を使用して、Action の型を定義します

ReturnType<typeof addTodo>
// ⇓
{
  type: "ADD_TODO";
  data: {
    id: number;
    text: string;
    complete: boolean;
  };
}
src/actions/todo/types.ts
type AddTodoAction = ReturnType<typeof addTodo>;
type ToggleCompletedAction = ReturnType<typeof toggleCompleted>;
type DeleteTodoAction = ReturnType<typeof deleteTodo>;

type TodoActions =
  | AddTodoAction
  | ToggleCompletedAction
  | DeleteTodoAction;
src/actions/filter/types.ts
type SetFilterAction = ReturnType<typeof setFilter>;

Reducer

引数の state, action の型定義のため、Redux.Reducer<State, Action> を使用します

初期化時には stateundefined が渡されるため、初期値を設定し
Action type で識別し、新しい State を返します

default case では引数 action を never 型に割り当てることで、絞り込みの漏れが無いようにしています
参考: TypeScript 2.0のneverでTagged union typesの絞込を漏れ無くチェックする

State の生成コードは前回と変わりないですね

src/store/reducer/todo/index.ts
const todoReducer: Redux.Reducer<TodoStateType, TodoActions> = (
  state = new Map(),
  action
) => {
  switch (action.type) {
    case ADD_TODO:
      return new Map(state.set(action.data.id, action.data));

    case TOGGLE_COMPLETED:
      const todo = state.get(action.data.id);

      if (todo) {
        return new Map(
          state.set(todo.id, { ...todo, complete: action.data.isCompleted })
        );
      }
      return state;

    case DELETE_TODO:
      state.delete(action.data);
      return new Map(state);

    default:
      const __check: never = action;
      return state;
  }
};

Filter の Action は 1種類だけなので if で

src/store/reducer/filter/index.ts
const filterReducer: Redux.Reducer<FilterStateType, SetFilterAction> = (
  state = "ALL",
  action
) => {
  if (action.type === SET_FILTER) return action.data;

  return state;
};

分割された Reducer を combineReducers を使用して1つにまとめます

src/store/reducer/index.ts
combineReducers({ todoReducer, filterReducer });

Store

Reducer を createStore に渡せば Store の完成です

src/store/index.ts
createStore(reducer);

型定義

前回の State 型と
Store 全体の型を定義します

src/store/types.ts
type TodoType = { readonly id: number; text: string; complete: boolean };
type TodoStateType = Map<number, TodoType>;

type FilterStateType = "ALL" | "COMPLETED" | "ACTIVE";

type StoreType = {
  todo: TodoStateType;
  filter: FilterStateType;
};

React との連携

私が React 触り始めるより前の話でしたが、hooks に対応したため、面倒な stateToProps だとか dispatchToProps なんかは書かずに、呆れるほど簡単に連携可能になりました

ここが一番面倒で厄介でよくわからん意味不明な所だったのでもうこれは革命です
Redux 全然よくわからんくないです、タイトル詐欺です
(一回 connect を使って書いちゃった後に気づいたのはひみつ)

まずはトップコンポーネントを Provider でラップし、store を渡します
これで、ネストされたコンポーネントで Redux にアクセス出来るようになります

src/components/app/index.tsx
import { Provider } from "react-redux";
...
const App: React.FC = () => {
  return (
    <Provider store={store}>
      <AddTodo />
      <ToggleFilter />
      <TodoList />
    </Provider>
  );
};

そうすれば後は以下の hooks で実際に Redux にアクセスし、State の取得、Dispatch を行うだけです

useSelector()

stateToProps に当たる機能です

引数として、selector 関数を受け取り、store の値を返します
selector 関数は引数に store を受け取り、値を返す関数です

TodoList での例

src/components/app/todo-list/index.ts
import { useSelector } from "react-redux";
...
const TodoList: React.FC = () => {
  // Todo のリストを取得
  const todoList = useSelector((store: StoreType) => store.todo);
  // 現在のフィルタを取得
  const filter = useSelector((store: StoreType) => store.filter);
  ...

useDispatch()

dispatchToProps に当たる機能です

store.dispatch() を返します

Todo での例
特定の Action のみ受け付けるよう型で縛ってます

src/components/app/todo-list/todo/index.ts
import { Dispatch } from "redux";
import { useDispatch } from "react-redux";
...
const Todo: React.FC<Props> = ({ todo }) => {
  const dispatch = useDispatch<Dispatch<ToggleCompletedAction | DeleteTodoAction>>();
  ...

まとめ

私の言う「Redux よくわからん」は十中八九 connect 周りの事だったので、hooks が使えたことで、大体 Prop で受け取っていた所を、useSelector, useDispatch に変更した感じになっちゃいました
記事書く前は使えるとか知らなかったんだもん...

それはそれとして、ディレクトリ構造とか型定義の場所とかを自分的に整理することができたので良かったです

趣旨ズレですが、Redux を使う利点もよく分かったと思います
TodoList とかが特に分かりやすい: Only React / With Redux

今回作成したコードは こちら (canoypa/react-redux-test-todo-app) にあります

Prev: とりあえず React だけで Todo
React (State) -> Redux の流れを掴むため、という名目で記事稼ぎのため一旦 React のみで Todo アプリをつくってます

参考

Redux入門【ダイジェスト版】10分で理解するReduxの基礎 、及びもと記事
Redux Docs
React Redux Docs

1
0
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
1
0