React-Reduxが難しい？ それは過去の話だ！  ～ ToDoアプリを最小限の労力で記述する ～

# Reduxが難しい？ それは過去の話だ！  ～ ToDoアプリを最小限の労力で記述する ～


　React-Reduxを簡単に利用するためのパッケージ[@jswf/redux-module](@jswf/redux-module)を利用して、最小限の労力でToDoアプリを作成するという内容です。

※追記
　@jswf/redux-moduleの使い方は以下記事で、もう少し短いソースにて確認できます
　[React-ReduxだけどReducerもActionも書かず、Dispatchすら使わず、データも何となく受け取れるようにする方法](https://qiita.com/SoraKumo/items/d7efd5d582a237ac440e)

## 最初に

　プログラムはファイル一つだけにまとめました。
　内容は以下のように構成されています。

- データ構造の定義
- データ操作用クラスの作成
- データ入力用コンポーネント
- データ表示用コンポーネント

　今回はReduxラッパーの機能を使うため、データの入力と表示はコンポーネントは分けてあります。
　ここで見ていただきたいのは、propsもuseStateも使っていないということです。
　全てのデータはReduxのStore上に格納されます。
　しかしReduxを使う上での前提となるFlux的な定義は一切書く必要がありません。
　ReduxModuleクラスを継承することによって、読み書き全ての手続きがReduxModuleで定義されているsetStateとgetStateメソッドに集約されます。

## 作ったもの
[実働サンプル](https://javascript-windowframework.github.io/redux-module-sample/dist/)
[GitHub上のソース](https://github.com/JavaScript-WindowFramework/redux-module-sample)
![screenshot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267629/96ada761-04c4-591d-250c-3d8e41b15534.gif)


## ソースコード
```tsx:index.tsx
import React from "react";
import * as ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import {
  ModuleReducer,
  useModule,
  ReduxModule
} from "@jswf/redux-module";

/**
 *データ構造の定義(TypeScript使用時)
 *
 * @export
 * @interface TodoState
 */
interface TodoState {
  //入力中データの保持
  input: {
    title: string;
    desc: string;
  };
  //TODOリスト
  todos: {
    id: number;
    title: string;
    desc: string;
    done: boolean;
  }[];
  //TODOのID附番表index
  index: number;
}

/**
 *Todoデータ管理用クラス
 *
 * @export
 * @class TodoModule
 * @extends {ReduxModule<TestState>}
 */
export class TodoModule extends ReduxModule<TodoState> {
  //初期値
  protected static defaultState: TodoState = {
    todos: [],
    input: { title: "", desc: "" },
    index: 0
  };
  /**
   *ToDoリストを返す
   *
   * @returns
   * @memberof TodoModule
   */
  public getTodos() {
    return this.getState("todos")!;
  }
  /**
   *ToDoを追加(追加データはStoreに入っているので引数不要)
   *
   * @memberof TodoModule
   */
  public addTodo() {
    //indexのインクリメント
    const index = this.getState("index")! + 1;
    this.setState({ index });
    //必要データの読み出し
    const title = this.getState("input", "title")!;
    const desc = this.getState("input", "desc")!;
    //todoを追加
    const todos = [
      ...this.getState("todos")!,
      { id: index, title, desc, done: false }
    ];
    this.setState({ todos })!;
  }
  /**
   *ToDoの状態管理
   *
   * @param {number} id
   * @param {boolean} done
   * @memberof TodoModule
   */
  public updateDone(id: number, done: boolean) {
    const srcTodos = this.getState("todos")!;
    //状態の書き換え
    const todos = srcTodos.map(todo =>
      todo.id === id ? { ...todo, done } : todo
    );
    this.setState({ todos });
  }
  /**
   *ToDoの削除
   *
   * @param {number} id
   * @memberof TodoModule
   */
  public remove(id: number) {
    const srcTodos = this.getState("todos")!;
    //削除データを除外
    const todos = srcTodos.filter(todo => todo.id !== id);
    this.setState({ todos });
  }
}


/**
 *入力フォームコンポーネント
 *
 * @returns
 */
function FormComponent() {
  const todoModule = useModule(TodoModule);
  return (
    <div style={{ textAlign: "center" }}>
      <div>
        <div>タイトル</div>
        <input
          style={{ width: "20em" }}
          value={todoModule.getState("input", "title")!}
          onChange={e => todoModule.setState(e.target.value, "input", "title")}
        />
        <div>説明</div>
        <textarea
          style={{ width: "20em", height: "5em" }}
          value={todoModule.getState("input", "desc")!}
          onChange={e => todoModule.setState(e.target.value, "input", "desc")}
        />
        <div>
          <button onClick={() => todoModule.addTodo()}>Todoを作成</button>
        </div>
      </div>
    </div>
  );
}

/**
 *ToDo出力コンポーネント
 *
 * @returns
 */
function TodoListComponent() {
  const todoModule = useModule(TodoModule);
  const todos = todoModule.getState("todos")!;
  return (
    <div style={{ display:"flex",flexDirection: "column" ,alignItems:"center"}}>
      {todos.map(todo => (
        <div
          key={todo.id}
          style={{
            display: "inline-block",
            width: "20em",
            marginTop: "1em",
            border: "solid 1px",
            textAlign:"center"
          }}
        >
          <div>
            {todo.id}:{todo.title}{" "}
            <span
              style={{ cursor: "pointer" }}
              onClick={() => todoModule.updateDone(todo.id, !todo.done)}
            >
              {todo.done ? "完了" : "未完了"}
            </span>
            {todo.done && (
              <span
                style={{ cursor: "pointer" }}
                onClick={() => todoModule.remove(todo.id)}
              >
                削除
              </span>
            )}
          </div>
          <div>{todo.desc}</div>
        </div>
      ))}
    </div>
  );
}

//Reduxに専用のReducerを関連付ける
//他のReducerと併用することも可能
const store = createStore(ModuleReducer);
ReactDOM.render(
  <Provider store={store}>
    <FormComponent />
    <TodoListComponent />
  </Provider>,
  document.getElementById("root") as HTMLElement
);

```

## まとめ

　ほぼReduxの影や形が消え去っています。
　今回の内容はコンポーネント間の連係や、データをStoreに集約することが目的であれば、かなり便利に使えると思います。

　パフォーマンスを考えて作る場合は、副作用の影響範囲を最小限に抑えるためデータ操作用のクラスを細分化したり、書き込みのみしか利用しないコンポーネント上ではwriteOnly属性を付けたりとチューニングが必要になりますが、それは別の記事で解説を入れたいと思います。

　Reduxの定義に疲れ果てた方はぜひ使ってみてください。

## リンク
- [@jswf/redux-moduleの解説](https://ttis.croud.jp/?uuid=76098143-9c49-4e85-959c-9617f3dcef8a)
- [Reduxがあまりにも面倒なので、何もかも隠蔽しつつ、その力を引き出すことにした](https://qiita.com/SoraKumo/items/99f22ac39baf8bd0c93a)
- [React-ReduxだけどReducerもActionも書かず、Dispatchすら使わず、データも何となく受け取れるようにする方法](https://qiita.com/SoraKumo/items/d7efd5d582a237ac440e)