Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
88
Help us understand the problem. What is going on with this article?
@SoraKumo

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

More than 1 year has passed since last update.

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

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

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

最初に

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

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

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

作ったもの

実働サンプル
GitHub上のソース
screenshot.gif

ソースコード

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の定義に疲れ果てた方はぜひ使ってみてください。

リンク

88
Help us understand the problem. What is going on with this article?
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
SoraKumo
TypeScriptでフロントエンドフレームワーク JWF(JavaScript-Window-Framework)を開発しています 世の中のWebシステムをSPA化するため、活動を続けています

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
88
Help us understand the problem. What is going on with this article?