TodoListアプリをReactを使用して作ってみました。

少しずつ、制作物を作ってくると点が増えてきてちょっとずつ線になってきたかなと思っていますが、色々調べるとまだまだ奥が深くて点が足りていません。

色々動いていてブログを書くのが遅くなってしまいました。
今日は、TodoListアプリを作成したのでコードと解説をしていきたいなと考えています。

制作物:TodoListアプリ ※データベースとは連携していないため、入力しても大丈夫です。
コード:GitHub (issue等いただければ嬉しいです。)
    codesandbox

今回追加した機能

TodoList追加機能

削除機能

編集機能

期限設定機能

完了・未完了ボタン

ソート機能

使用した技術

React
Redux

UUID

Next.js

– Material-UI

– tailwindcss

DatePicker

課題

jestの使い方ができていなかった

TSの型定義についてもう少し深掘りする必要がある

フォルダ・ファイルの構造をある程度固める必要がある

今後やろうとしていること

値をlocal strageなどに保存できるようにする

DB連携

Docker導入

ユーザー機能の追加

Circle CI/CD 導入

useCallBack,useMemoを使用してパフォーマンスの向上を図る

作成していて苦労した点

_app.jsについて

_app.jsはNext.jsのカスタムAppコンポーネントであり、
コンポーネント全体に共通の設定や機能を提供する役割を持つようです。

Reduxのstoreを全てのページに提供するために、<Provider>コンポーネントを使ってstoreをラップする必要がありました。

グローバルな設定を行うことで、Reduxの状態管理をアプリ全体で利用できるようになりました。

下記が私が書いた実際のコードです。

// _app.js

import React from 'react';
import { Provider } from 'react-redux';
import { store } from '../redux/store';
import Modal from 'react-modal';
import '../styles/global.css';
import '../styles/DatePickerOverrides.css';

// Modalの設定
Modal.setAppElement('#__next');

interface MyAppProps {
  Component: React.ComponentType<any>;
  pageProps: any;
}

const MyApp: React.FC<MyAppProps> = ({ Component, pageProps }) => {
  return (
    <Provider store={store}>
      <React.StrictMode>
        <Component {...pageProps} />
      </React.StrictMode>
    </Provider>
  );
}

export default MyApp;

このファイルで行なったこととしては、

・また、解説することになりますが、storeを宣言しているファイルをインポートする

・storeを全てのページに提供するために<Provider>コンポーネントを使用してstoreをラップする

となります。

stateの宣言の仕方が最新のバージョンのReactだと違った件

stateの宣言の仕方が2つありました。

  • クラスコンポーネントでの作成
  • 関数コンポーネントでの作成

今までの学習で学んだことは、クラスコンポーネントでの作成の仕方でした。

// サンプル

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: 'にんじゃわんこ' };
  }
}

上記の例では、

Reactのクラスコンポーネントを使用していました。
constructorメソッド内でthis.stateを初期化してコンポーネントの状態を宣言して、状態が変化すると再レンダリングされていました。
この方法はReactの以前のバージョンで主流でした。コードが冗長で複雑になりがちでした。

一方の関数コンポーネントでの作成の仕方は、

// サンプル

import React, { useState } from "react";
const [inputValue, setInputValue] = useState<string>('');

上記の例では、

Reactの関数コンポーネントとフックを使用していました。
useStateフックは関数コンポーネント内で状態を宣言し、その初期値を設定しています。
React 16.8以降のバージョンから導入されたもので、コードがよりシンプルになりました。

さらに、フックを使用することで、状態を関数コンポーネント内で管理できるようになるようでした。

結論として

React 16.8より下のバージョンであれば、クラスコンポーネントで宣言したらいいのかなと思います。

それ以外の場合は、関数コンポーネントになるのかなと思いました。

学習していたReduxで宣言するstate,action,reduce定義をcreateSliceで作成できるという点

学習した際は、Reduxで宣言するstate,action,reduce定義をバラバラで定義していましたが、

createSliceを使用すると一括で定義することができました。

例としては、下記のようになります。

  • state,action,reduce定義をバラバラで定義する場合
// sample code

// stateの初期値を定義
const initialState = {
  counter: 0,
};

// actionのtypeを定義
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// action creatorを定義
const incrementAction = () => {
  return {
    type: INCREMENT,
  };
};

const decrementAction = () => {
  return {
    type: DECREMENT,
  };
};

// reducerを定義
const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case INCREMENT:
      return {
        ...state,
        counter: state.counter + 1,
      };
    case DECREMENT:
      return {
        ...state,
        counter: state.counter - 1,
      };
    default:
      return state;
  }
};

上記のコードでは、

typeやreducer、actionの定義等を今まで通りしていました。

  • createSliceで定義する場合
// sample code

import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    counter: 0,
  },
  reducers: {
    increment: (state) => {
      state.counter += 1;
    },
    decrement: (state) => {
      state.counter -= 1;
    },
  },
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

こちらのコードの場合、

createSlice()メソッドの中でtypeやreducer、actionの定義をまとめて行うのでコードが一気に短くなりました。

  • createSlice()の使用の仕方
  1. name: スライス(Slice)の名前を指定します。スライスはReduxのstateの一部を指すことを意味します。例えば、Todoアプリケーションの場合、**todos**という名前のスライスを作成することができます。
  2. initialState: スライスの初期状態を指定します。Reduxのstateの初期値を定義します。
  3. reducers: スライスに対するReducerとActionを定義するオブジェクトです。オブジェクトの各プロパティがActionのtypeに対応し、その値がReducer関数として定義されます。

createSlice“はこの情報を元に、ReducerとActionを自動的に生成します。

その値を、使用する各ファイルでimportする方になります。

私の作成したTodoListのコードを例に挙げると

// todosReducer.ts

import AllTodoDataProps from '../types/AllTodoDataProps';
import Todo from "../types/Todo";
import TodoOptionData from '../types/TodoOptionData';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

const initialState: AllTodoDataProps = {
  allTodoData: {
    OptionData: {
      sortBy: "addOrder",
      sortOrder: "asc",
    },
    ListData: {},
  }
};

const todosReducer = createSlice({
  name: 'todos',
  initialState: initialState,
  reducers: {
    // 他のreducerの設定を追加することもできます
    addTodo: (state, action: PayloadAction<Todo>) => {
      const { id, ...payload } = action.payload;
        state.allTodoData.ListData[id] = { id, ...payload };
    },

    deleteTodo: (state, action: PayloadAction<string>) => {
      const id = action.payload;
      if (state.allTodoData.ListData) {
        delete state.allTodoData.ListData[id];
      }
    },

    completeTodo: (state, action: PayloadAction<{ id: string; isComplete: boolean }>) => {
      const {id, isComplete } = action.payload;
      if (state.allTodoData.ListData) {
        state.allTodoData.ListData[id].completed = isComplete;
      }
    },

    editTodoAction: (state, action: PayloadAction<Todo>) => {
      const { id, ...payload } = action.payload;
      if (state.allTodoData.ListData) {
        state.allTodoData.ListData[id] = { id, ...payload };
      }
    },

    showEditForm: (state, action: PayloadAction<{ id: string; isEditFormVisible: boolean }>) => {
      const {id, isEditFormVisible} = action.payload;
      if (state.allTodoData.ListData) {
        state.allTodoData.ListData[id].isEditFormVisible = isEditFormVisible;
      }
    },

    changeSort: (state, action: PayloadAction<TodoOptionData>) => {
      const { sortBy, sortOrder } = action.payload;
      state.allTodoData.OptionData.sortBy = sortBy;
      state.allTodoData.OptionData.sortOrder = sortOrder;
    },

    clearAllTodos: (state) => {
      state.allTodoData.ListData = {}
    }
  },
});

export const { addTodo, deleteTodo, completeTodo, editTodoAction, showEditForm, changeSort, clearAllTodos } = todosReducer.actions;
export default todosReducer.reducer;

となりました。

Reduxの開発ツールで見ると、

となっており、

todosという名前のスライス名が作られていました。

他の部分は、

todosReducer.tsというファイルで初期値として定義した部分になります。

・sortBy

・sortOrder

上記については、ソート機能として初期値で設定する値になります。

また、ListDataは、ここにTodoListアプリで追加したデータが追加されていきます。

storeを宣言しようとして、createStoreを使用したらエラーが出た

createSliceはRedux Toolkitに含まれるユーティリティであり、storeを定義することはできなかったです。

Redux Toolkitを使用する場合、configureStoreというメソッドを使用してReduxのStoreを作成することが推奨されていました。configureStoreはReduxのStoreを作成する際に必要な設定を自動的に行ってくれるため、createStoreよりも便利でした。
また、Redux DevToolsとの統合なども自動的に行われました。

私が書いたコードは下記のようになります。

// store.ts

import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosReducer';

export const store = configureStore({
  reducer: {
    todos: todosReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;

todosReducer以外のreducerを作成した場合どうするのかという疑問

TodoListのアプリで行うStoreへの処理(action,dispatch,reduce)は、todosReducer.tsに記述していました。

仮に、今後ユーザー機能のreducerなどを作成した際にどうするのか気になったので調べてみました。

結論としては、

それぞれ作成したreducerをまとめるという結論に至りました。

ですので、reducerをまとめるreducers.tsというファイルを作成して、

下記のようなコードを記述しました。

// reducers.ts

import { combineReducers } from 'redux';
import todosReducer from './todosReducer';

const rootReducer = combineReducers({
  todos: todosReducer,
});

export default rootReducer;

reducerを宣言している各ファイルをインポートして、

combineReducers()という関数でまとめるという感じでしょうか。

ここまでのまとめ

色々ファイルを分けてしまったため、わかりにくくなったのですが、

・store.tsで、必要なStoreを定義する。

・todosReducer.tsで、createSliceメソッドを用いて、reducerを定義する。

・reducers.tsで、他にもreducerを作成した時のために、combineReducersメソッドを用いてreducerをまとめるファイルを作成する。

状態を更新する前の値と更新した後の値の操作で結果が違うことについて

状態の更新をめぐって意図していない結果やエラーが出ることはReact,Reduxを使用するようになってから頻回に出ました。

実例としては、

・アクションをディスパッチするコードの後に書かれたコードは、アクションの処理が完了するまで実行されませんでした。

意図した値ではなかった場合の例

setEditedTodo({ ...editedTodo, isEditFormVisible: false })
dispatch(editTodoAction(editedTodo));

上記のコードは、
editedTodoオブジェクトのisEditFormVisibleプロパティを変更してから、同じeditedTodoオブジェクトを使ってeditTodoActionをディスパッチするようにしてたようでした。(本当はそんなつもりはなかったのですが笑)
しかし、setEditedTodoは非同期的に状態を更新するため、dispatch(editTodoAction(editedTodo))が実行される時点では、setEditedTodoで更新されたeditedTodoの値が反映されていない可能性がありました。

では、どうしたのか、、、

意図した値になった場合の例

const updatedTodo = { ...editedTodo, isEditFormVisible: false };
setEditedTodo(updatedTodo);
dispatch(editTodoAction(updatedTodo));

上記の例では、

1、まず新しいオブジェクトupdatedTodoを作成し、その後setEditedTodoで状態を更新するようにしました。
2、その後、dispatch(editTodoAction(updatedTodo))を呼び出しました。
これによって、状態の更新とアクションのディスパッチのタイミングが正しく整合し、更新された値がアクションに正しく渡されました。

何がいけなかったのか

Reduxのアクションが非同期を含む場合は、状態の更新とアクションのディスパッチのタイミングに気を付ける必要がありました。非同期タスクが終了した後にディスパッチする場合は、タスクが完了してからアクションをディスパッチすることが大事でした。ß

モーダル(react-modal)のエラー

Warning: react-modal: App element is not defined. Please use `Modal.setAppElement(el)` or set `appElement={el}`. This is needed so screen readers don’t see main content when modal is opened. It is not recommended, but you can opt-out by setting `ariaHideApp={false}`.

というエラーが出た。

chatGPTによると、

この警告は、react-modalコンポーネントを使用する際に、モーダルが開かれたときにスクリーンリーダーがメインコンテンツを認識してしまうのを防ぐために、アプリケーションのルート要素を指定する必要があることを示しています。

とのことだった。

※ スクリーンリーダー : 視覚障害者や視覚に障害のある人々がコンピューターやモバイルデバイスを使用する際に、情報を音声または点字で提供する補助技術。

※ ルート要素 : ReactアプリケーションのUI全体が描画されるHTML要素のこと。Reactアプリケーションのコンポーネントツリーがレンダリングされる最上位の要素です。

調べたところ、

“react-modal”では、モーダルが開いている間、モーダルの外側のコンテンツをスクリーンリーダーに認識させないために、アプリケーションのルート要素を指定する必要があるようでした。
これにより、モーダルが開いたときにスクリーンリーダーがモーダル内のコンテンツに対応します。

解決策としては2個ありました。

  • “react-modal”を他のページで使う時など(アプリケーション全体で)はapp.jsはindex.jsで記述する。
  • react-modalコンポーネントの使用箇所でappElementプロパティを指定してアプリケーションのルート要素を指定する

今回は、app.jsやindex.jsに記述する方法を試しました。

import React from 'react';
import { Provider } from 'react-redux';
import { store } from '../redux/store';
import Modal from 'react-modal';
import '../styles/global.css';
import '../styles/DatePickerOverrides.css';

// Modalの設定
Modal.setAppElement('#__next');

interface MyAppProps {
  Component: React.ComponentType<any>;
  pageProps: any;
}

const MyApp: React.FC<MyAppProps> = ({ Component, pageProps }) => {
  return (
    <Provider store={store}>
      <React.StrictMode>
        <Component {...pageProps} />
      </React.StrictMode>
    </Provider>
  );
}

export default MyApp;

としました。

Modal.setAppElement(‘#__next’)について

Modal.setAppElement('#__next')
  • “react-modal”ライブラリの一部で、モーダルが開かれたときにスクリーンリーダーがメインコンテンツを認識しないようにするために、アプリケーションのルート要素を指定する役割を果たします。`#__next`というIDを指定することで、Next.jsアプリケーションのルート要素を指定することになります。
  • #__next、はNext.jsの内部的な実装詳細であり、ユーザーが直接指定する必要はありません。react-modalが正しく機能するために、単にModal.setAppElement(‘#__next’)`と書くだけでOK。このコードは、モーダルが開かれた際にスクリーンリーダーがメインコンテンツを認識しないようにするためのもの。

react-modalだけに限らず、他のライブラリでも同様のことが起こるかもしれないとの記事もありました。

今回は、作成したTodoListアプリのコードについて復習としてまとめてみました。

・Reactフックについて

・状態の変更

・Ruduxの動作について

・Next.jsの基本的な機能について

何個もアプリやサイトを実装してみて、理解や改善点を見つけて知識を深めていかないといけないなと感じました。

今日はここまでです〜。

レビューなどお待ちしています。