Help us understand the problem. What is going on with this article?

Redux Starter KitでHooksとReduxを使いこなそう



Redux Sterter KitはRedux-Toolkitに名称変更されてAPIも変更されています。本記事はそのままでは最新版redux-toolkitとして動作しません。
https://github.com/reduxjs/redux-toolkit

はじめに

React-Reduxの公式から「Redux Starter Kit」というものが公開されています。

これがなにかといえば、私の理解するかぎり以下です。

React-Reduxまわりのベストプラクティス、定番拡張、定番併用ライブラリ、定番ミドルウェアを、簡単に組込むための簡単で軽めのライブラリ、メタパッケージ。

CLIコマンドではなくライブラリです。
これは良いものだと思いましたので全力でお勧めしていきます。

特徴

  • TypeScriptフル対応。当然ですね。
  • React Redux 7.1対応、つまりHooks対応。これからはHooksで生きていく。Hooksの無い人生は考えられない。
  • Immerが組込まれることで、直接state変更可能になり、reducer記述が簡潔になる。これはイイ! 一番の推しポイントかもしれない。MobXの利点を一部とりこんだと言えるのかも??(追記、後で知ったのですが、MobxとImmerの作者は同じ) Reduxが冗長? 昔の話です。🎉🎉🎉🎉
    • Immutable.jsとくらべてImmerのいいところは、Immutable.jsのように記述が特殊にならないことが一つと、他に、同じぐらい重要なのは、他の箇所にImmutable.jsの要求するデータ構造を感染させていかないこと。非侵襲的。重要。
  • Sliceなるものでモジュール化できる。
    • SliceはRedux Ducksが提供するものと似たモジュール概念。同じではないらしい。
    • Sliceは以下を束ねたもの。
      • Redux Stateのトップレベルのslice。ただしこれはオプショナルで、sliceにわけないこともできる。
      • それぞれごとの、combineReducerでまとめる元となるそれぞれのReducer群
      • sliceごとのState初期値
  • Sliceからaction creatorは型付きで自動生成される🎉🎉🎉🎉
    • ACTION定義は完全に消える。action定数も。creatorも。1行も。一行もだ。Reduxが冗長? 昔の話です。🎉🎉🎉🎉
  • これはReact-Reduxの機能の良さからだが、
    • useSelector呼び出すhookをslice Module側で定義すれば、もうほんとにstateとりだしは楽です。
  • デフォルトで今は「[immutableStateInvariant, thunk, serializableStateInvariant]のミドルウェアが設定される。thunkは議論あるでしょうが、使わないことももちろん可能。
  • Redux DevToolsの設定がデフォルトで入っている。
  • 上記のようなことのワイアリングが1パッケージでできる。🎉🎉🎉🎉

上記はこれだけでも素晴らしいと思うのですが、さらに公式から、というのが安心感があって良いです。この形のReduxがデファクトスタンダードと扱われるようになることを期待します。

コード例

まずは、TodoMVCみたいなものを作るとします。そのときのタスクリストをredux stateとしてあつかうとします。そのためにTodoをあつかうためのモジュールをこんな風に定義します。

todoModule.ts
import { createSlice } from "redux-starter-kit";
import { useSelector } from "react-redux";

export type TodoItem = {
    title: string;
    completed: boolean;
    key: string;
}
// createSlice() で actions と reducers を一気に生成
const todoModule = createSlice({
    // slice: "todo",
    initialState: [] as TodoItem[],
    reducers: {
        addTodo: (state, action: { payload: TodoItem }) => {
            state.push(action.payload)
        },
        removeTodo: (state, action: { payload: string }) => {
            return state.filter((item) => item.key !== action.payload);
        },
        setCompleted: (state, action: { payload: { completed: boolean, key: string } }) => {
            state.forEach((item) => {
                if (item.key === action.payload.key) {
                    item.completed = action.payload.completed;
                }
            });
        },
    }
});

export const useTodoItems = () => {
    return useSelector((state: ReturnType<typeof todoModule.reducer>) => state);
}
// 他に便利なuseSelect呼び出しや、粒度の細かいaction creatorのexportをここに追加していってもいい。

export default todoModule;

Immerのおかげで、reducerが劇的に簡潔になってます。stateをまるごとおきかえるときはreturnで返します。あとは代入などで副作用バリバリで書いてますがProxy経由なので実際にはStateを変更していません。

上ではcombineReducerも使わないという前提でsliceプロパティは定義してません。もちろん規模が大きくなれば使ってもかまいません。使ったとしてもsliceの存在は最後のuseTodoItemsで隠蔽できます。
ちなみに、dispatchでまるごと公開するのは楽とはいえ粗い、という人は、ちまちまとaction creatorを公開していくこともできます。お好みで。

一つだけ、この記事で独自に提案したいこととして、useSelectorの呼出は、このモジュール側でHooks(ここではuseTodoItems)として定義して、使いやすくしたものをexportするということです。特に、useSelectorの直接呼出は型定義に関して長く煩雑になりがちで、こちらに隠蔽するメリットがあります。

次に、上に含まれるreducerから以下のようにstoreを構築します。デフォルトのミドルウェアが組み込まれますが、もちろん好きなものを組みこめもします。

store.ts
import { configureStore } from "redux-starter-kit";
import todoModule from "./modules/todoModule";

export const setupStore = () => {
    const store = configureStore({
        reducer: todoModule.reducer
    });
    return store
}

そのstoreから以下のようにProviderコンポーネントでアプリに組み込んで、

App.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from "react-redux";
import { setupStore } from "./store";

const store = setupStore();

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root'));
serviceWorker.unregister();

準備は完了。
あとは、それぞれのコンポーネントからstoreの値をつかいたければ、

import { useTodoItems } from "./modules/todoModule";
  :
  const todos = useTodoItems();

actionをディスパッチしたければ、

import { useDispatch } from "react-redux";
import todoModule from "./modules/todoModule";

  
  const dispatch = useDispatch();

  dispatch(todoModule.actions.addTodo({ title: "TITLE", completed: false, key: 'XXX' }))  

  const todos = useTodoItems();
  const todo = todos[key];

  dispatch(todoModule.actions.setCompleted({ key: todo.key, completed: !todo.completed }));
  dispatch(todoModule.actions.removeTodo(todo.key));

のように、任意のコンポーネントからも自由自在です。connect, mapStateToProps, mapDispatchToPropsよさらば。

以上です。ザッツオール。ナッシングエルス。驚異的に簡単になったわけです。

コンポーネント間で状態を共有する場合は、間違いなく素のuseStateでやるよりも簡単になります。あっちはImmer使えないですし1、stateのsetterをプロパティで取り回したりイベントハンドラとりまわすのはつらいですからね。

参考


  1. こちらにあるような各種Hooks(useArrayとか)を使えば、Immerに匹敵するぐらいには楽かもしれない。 

uehaj
React、TypeScript、Rust、Dart、Elm、Haskell、ES2020、Groovyが好き。
http://d.hatena.ne.jp/uehaj/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away