1. トップページ
  2. React と Redex でTODOアプリを作ってみる

React と Redex でTODOアプリを作ってみる

開発環境について

以下の環境、ソフトウェアで開発を行いました。
Ant Desing はUIライブラリのため必須ではありませんが、
TODOアプリに最低限必要なコンポーネントがすべて用意されているため、
開発期間の短縮を目的に使用させていただきました。

React 17.0.2
Redux 7.2.4
Ant Desing 4.16.6


成果物


Reduxについて

歴史のあるライブラリのため、公式ドキュメントも充実していますし、
わかりやすく紹介されているブログもたくさんあるため、ここでは簡単な説明のみを行います。

Reduxとは

Action と呼ばれるイベントを使用してアプリケーションの状態の管理や更新を行うライブラリです。

Store と呼ばれる大きな箱が1つだけ用意されており、そこで State の管理を行います。
どのコンポーネントからでも取得と更新が可能であるため、
個々のコンポーネントの状態管理としては使用せず、
アプリケーション全体の状態管理として使用するのに適しています。

Store の更新には大きくわけて3つの手続きを踏む必要があり、
必ず左から順番に実行されるという特徴を持ちます。

Action > Reducer > Store

そのため、データの流れが追いやすくなるというメリットがあります。
ただ、複数回の手続きが必要になるためどうしてもコード量が増えてしまいます。
小規模なアプリに採用しても時間がかかるだけなので、中規模以降のアプリに採用すべきです。

3つの原則

Redux には3つの原則があります。
これらのルールを守ることでバグの発生を防ぐことができます。

Single source of truth
State is read-only
Changes are made with pure functions

Three Principles

Redux Toolkitについて

今回は Redux React-Redux だけを使用しているため、コードの総量がかなり多くなっていますが、
Redux Toolkit というパッケージを併用すれば、もっと簡単に書くことが可能です。

公式ドキュメントでも使用が推奨されているため、
新しいプロジェクトを立ち上げる場合は併用することをオススメします。
Getting Started with Redux Toolkit

Actionを定義する

まずは Action を定義しましょう。

最終的に、以下のようなオブジェクトを用意する必要があり、
これを Actionオブジェクト と呼びます。

 return {
    type: "起こしたいアクション名",
    payload: "ユーザーが行った操作"
};


Action Typesを定義する

起こしたい Action の名前を決めます。
ユニークな値でなければならないため、定数として定義いたします。

const CREATE_TASK = "CREATE_TASK";
const DELETE_TASK = "DELETE_TASK";


Action Creatorsを定義する

行いたい Action とユーザーが操作した値が返却される関数を作成します。

export const createTask = (task) => {
  return {
    type: CREATE_TASK,
    payload: task
  };
};
export const deleteTask = (id) => {
  return {
    type: DELETE_TASK,
    payload: id
  };
};


Reducersを定義する

次に Reducers を定義します。

Reducer関数を定義する

現在の状態 todo と、先程定義した Action の2つを引数として受け取ります。
Reduxは初回起動時には状態を持っていません。initialState を代入して状態を作成します。

const initialState = [];

function taskReducer(todo = initialState, action) {
  const { type, payload } = action;
  switch (type) {
    case CREATE_TASK:
      return [...todo, payload];
    case DELETE_TASK:
      return todo.filter((t) => t.id !== payload);
    default:
      return todo;
  }
}


複数のReducersを1つにまとめる

combineReducers という関数を使用すれば複数の Reducers を1つにまとめることが可能です。
今回は Reducers が1つしかないため使用する必要はありませんが、
特に動作への影響はなく、後で Reducers が追加された際に書き直しが不要となるため使用しています。

import { combineReducers } from "redux";

export const rootReducer = combineReducers({
  task: taskReducer
});


Storeを作成

次に状態管理を行う Store を定義します。

ストアインスタンスを作成する

createStore を実行し、ストアインスタンスを作成します。
第1引数に定義したReducers の rootReducer、第2引数に初期状態を示す変数  initialState を指定して実行します。

import { createStore } from "redux";

const initialState = {};
const store = createStore(rootReducer, initialState);

export default store;


ReactにStoreを提供

<App /><Provider> をラップします。
ここまでの繋ぎこみが完了すれば、以降は Action が実行されるたびに状態の更新が行われます。

import ReactDOM from "react-dom";
import App from "./App";

import { Provider } from "react-redux";
import store from "./store";
import "antd/dist/antd.css";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>,
rootElement
);


状態管理を行う

コンポーネント内で Store の取得と更新を行います。どちらも専用の関数を使用します。

タスクの仕様

先にタスクがどのような形式で保存されるのか解説致します。
タスクの内容と、固有のIDを持ったオブジェクト形式で保存を行います。
IDは generateId という関数を使って作成しています。

{
  task: "タスクの内容"
  id: 12345678
}


状態の取得を行う

Store の状態を取得するには useSelector という関数を使用します。

import { useSelector } from "react-redux";
const App = () => {
  const tasks = useSelector((state) => state.task);
}


状態の更新を行う

Store の状態を更新するには useDispatch という関数を使用します。

import { useDispatch } from "react-redux";
const App = () => {
    const dispatch = useDispatch();
    let normalize = {
        task: task,
        id: generateId()
    };
    dispatch(createTask(normalize));
}


タスクの削除

タスク削除用関数の引数に id を指定した状態で実行します。
この関数は保存済みタスクの id と引数の id を比較、一致しないタスクのみを返却します。

dispatch(deleteTask(item.id))


全体のコード

全体のコードは以下のようになります。コピペいただければそのまま動作します。

import React, { useState, useEffect } from "react";
import { Button, Input, Row, Col, Typography, List } from "antd";
import { useDispatch, useSelector } from "react-redux";
import { createTask, deleteTask } from "./redux";
import "./styles.css";

const App = () => {
  const [task, setTask] = useState("");
  const [inputEmptyError, setInputEmptyError] = useState(false);

  const dispatch = useDispatch();
  const tasks = useSelector((state) => state.task);

  useEffect(() => {}, [dispatch]);

  const generateId = () => {
    const strong = 1000;
    return (
      new Date().getTime().toString(16) +
      Math.floor(strong * Math.random()).toString(16)
    );
  };

  const handleInput = (e) => {
    const value = e.target.value;
    setTask(value);
  };

  const handleSubmit = () => {
    if (!task) {
      setInputEmptyError(true);
      return;
    } else {
      setInputEmptyError(false);
    }
    let normalize = {
      task: task,
      id: generateId()
    };
    dispatch(createTask(normalize));
    initialForm();
  };

  const initialForm = () => {
    setTask("");
  };

  return (
    <div className="App">
      <div className="container">
        <section>
          <List
            className="taskList"
            itemLayout="horizontal"
            dataSource={tasks}
            renderItem={(item) => (
              <List.Item
                actions={[
                  <Button onClick={() => dispatch(deleteTask(item.id))}>
                    削除
                  </Button>
                ]}
              >
                {item.task}
              </List.Item>
            )}
          />
        </section>
        <section>
          <Row>
            <Col span={20}>
              <Input
                name="task"
                onChange={handleInput}
                value={task}
                placeholder="タスク内容を入力"
              />
              {inputEmptyError && (
                <Typography.Text type="danger">
                  タスク内容は入力必須です。
                </Typography.Text>
              )}
            </Col>
            <Col span={4}>
              <Button type="primary" onClick={handleSubmit} block>
                追加
              </Button>
            </Col>
          </Row>
        </section>
      </div>
    </div>
  );
};

export default App;


最後に

より詳しい情報が知りたくなったときは公式ドキュメントを参照してください。
以上で解説は終了です。お疲れ様でした。

参考にさせていただいたページ