LoginSignup
1
6

More than 3 years have passed since last update.

30分で習うより慣れる! React + Redux Toolkit + TypeScript の Todoアプリチュートリアル その1

Last updated at Posted at 2021-04-18

はじめに

最近 React を勉強し始めた新参者です。
:older_man:「とりあえず TypeScript で始めよう → Redux って便利そう → Redux Toolkit ってのがあるのか~ ・・・・・・・・・ :angel:
という具合にインターネットを潜ってチュートリアル始めたら、情報が多かったり古かったりEnglishだったりでカオスったので、習うより慣れるチュートリアルをここに残します。
習うより慣れたい皆様の参考になれば幸いです。

:computer: 環境

% sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7 //Catalinaさん、そろそろ上げないと、、、
BuildVersion:   19H2

% brew -v
Homebrew 3.0.11

% nodebrew -v
nodebrew 1.0.1

% node -v
v14.16.1

% yarn -v
1.22.10

:trumpet: Start!!

プロジェクト作成

Redux+TypeScript テンプレートを使用

:point_down: こちら

yarn create react-app redux-sample --template redux-typescript
作成したプロジェクトへ移動
cd redux-sample

テンプレートに含まれてるRedux関連のモジュール達です

% ls -d -e node_modules/*redux*
node_modules/@reduxjs       node_modules/react-redux    node_modules/redux      node_modules/redux-thunk

Redux を TypeScript に対応させるやつをインストールします (雑

yarn add typescript-fsa typescript-fsa-reducers

以下の構成が出来上がります
ディレクトリ構成

~/redux-sample
% tree -I ".DS_Store|node_modules|.git"  -La 4 --dirsfirst
.
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── app
│   │   ├── hooks.ts
│   │   └── store.ts
│   ├── features
│   │   └── counter
│   │       ├── Counter.module.css
│   │       ├── Counter.tsx
│   │       ├── counterAPI.ts
│   │       ├── counterSlice.spec.ts
│   │       └── counterSlice.ts
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   ├── serviceWorker.ts
│   └── setupTests.ts
├── .gitignore
├── README.md
├── package.json
├── tsconfig.json
└── yarn.lock

これを以下に変更します (こちらはお好みでどうぞ)
新ディレクトリ構成

.
├── public
│   └── # 省略
├── src
│   ├── app
│   │   ├── hooks.ts
│   │   └── store.ts
│   ├── features
│   │   └── counter
│   │       ├── counterAPI.ts
│   │       ├── counterSlice.spec.ts
│   │       └── counterSlice.ts
│   ├── pages # <-- 作成
│   │   ├── assets # <-- 作成
│   │   │   └── logo.svg # <-- 移動
│   │   ├── counter # <-- 作成
│   │   │   ├── Counter.module.css # <-- 移動
│   │   │   └── Counter.tsx # <-- 移動
│   │   ├── App.css # <-- 移動
│   │   ├── App.test.tsx # <-- 移動
│   │   └── App.tsx # <-- 移動
│   ├── index.css
│   ├── index.tsx
│   ├── react-app-env.d.ts
│   ├── serviceWorker.ts
│   └── setupTests.ts
└── # 省略

既存のソースをちょっと修正する

src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './pages/App'; // <-- 修正
// 以下省略
src/pages/App.tsx
import React from 'react';
import logo from './assets/logo.svg'; // <-- 修正
import { Counter } from './counter/Counter'; // <-- 修正
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        {/* --- ここから削除 ---
        <Counter />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        --- ここまで --- */}
        <span>
// 以下省略
src/pages/counter/Counter.tsx
import React, { useState } from 'react';
// 省略
} from '../../features/counter/counterSlice'; // <-- 修正
import styles from './Counter.module.css';

export function Counter() {
  //省略

  const incrementValue = Number(incrementAmount) || 0;

  return (
    <div>
      <div className={styles.row}>
        // 省略
      </div>
      {/*  --- ここから追記 --- */}
      <p>
        Edit <code>src/pages/counter/Counter.tsx</code> and save to reload.
      </p>
      {/*  --- ここまで ---  */}
    </div>
  );
}

TodoアプリのUI作成

Todoアプリ用のページディレクトリを作成

mkdir src/pages/todo

Todoアプリのベースを作成

src/pages/todo/TodoApp.tsx
import React from 'react';
import AddTodo from './components/AddTodo'

function TodoApp() {
  return (
    <div>
      <AddTodo />
      <p>
        Edit <code>src/pages/todo/TodoApp.tsx</code> and save to reload.
      </p>
    </div>
  );
};

export default TodoApp;

Todo 入力用の Input と Button を作成

src/pages/todo/components/AddTodo.tsx
import React from "react";

export default function AddTodo(): JSX.Element {
  const [text, setText] = React.useState("");

  function handleChange(e: { target: HTMLInputElement }) {
    setText(e.target.value);
  }

  function handleSubmit(e: any) {
    e.preventDefault();

    if (!text.trim()) {
      return;
    }

    setText("");
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={handleChange} />
      <button type="submit">Add Todo</button>
    </form>
  );
}

ルーティングの実装

ルーティングモジュールをインストール

yarn add react-router-dom  @types/react-router-dom

App.tsx から各ページを URL 毎に読み込むように修正

src/pages/App.tsx
import React from 'react';
import logo from './assets/logo.svg';
import { BrowserRouter as Router, Route } from 'react-router-dom'; // <-- 追記
import { Counter } from './counter/Counter';
import Todo from './todo/TodoApp'; // <-- 追記
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        // --- ここから追記 ---
        <Router>
          <Route exact path="/" component={Counter} />
          <Route exact path="/todo" component={Todo} />
        </Router>
        // --- ここまで ---
        // 省略
      </header>
    </div>
  );
}

export default App;

ロゴの主張が強いので以下の css を修正

src/App.css
.App-logo {
  height: 20vmin;    /* 40 --> 20 */
  /* 省略 */
}

.App-header {
  min-height: 10vh; /* 100 --> 10 */
  /* 省略 */
}

起動して見た目を確認

yarn run start

:point_down: localhost:3000/ にアクセスしたときの見た目 (サンプルソース)

image.png

:point_down: localhost:3000/todo にアクセスしたときの見た目

image.png

Todoを追加できるようにする

Todoリストの componet を作成

  • Todoリスト
src/pages/todo/components/TodoList.tsx
import * as React from "react";
import TodoItem from './TodoItem'

export default function TodoList() {
  const text: any = ""
  return (
    <ul>
      <TodoItem {...text} />
    </ul>
  );
}
  • Todo単体
src/pages/todo/components/TodoItem.tsx
import * as React from "react";

interface TodoProps {
  text: string;
}

export default function TodoItem( { text }: TodoProps ) {
  return (
    <li>
      {text}
    </li>
  );
}
  • 作成した Todo Components を読み込む
src/pages/todo/TodoApp.tsx
import React from 'react';
import AddTodo from './components/AddTodo'
import TodoList from './components/TodoList' // <-- 追記

export default function TodoApp() {
  return (
    <div>
      <AddTodo />
      <TodoList /> {/* <-- 追記 */}
      <p>
        Edit <code>src/pages/todo/TodoApp.tsx</code> and save to reload.
      </p>
    </div>
  );
};

起動して見た目を確認

:point_down: Todoリストの表示部分ができました

image.png

入力した値をTodoリストに出力する

Slice を作成

src/features/todo/todoSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AppThunk, AppDispatch, RootState } from "../../app/store";
import { Todo } from './types'

const initialState: Todo[] = [];

const todoSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    addTodo(state, action: PayloadAction<Todo>) {
      state.push(action.payload);
    },
  }
});

export const addTodo = (text: string): AppThunk => async (
  dispatch: AppDispatch
) => {
  const newTodo: Todo = {
    id: Math.random().toString(36).substr(2, 9),
    text: text
  };
  dispatch(todoSlice.actions.addTodo(newTodo));
};

export default todoSlice.reducer;
  • Todo Interface type を作成
src/features/todo/types.ts
export interface Todo {
  id: string;
  text: string;
}

Reducer を作成

src/app/rootReducer.ts
import { combineReducers } from "@reduxjs/toolkit";
import counter from "../features/counter/counterSlice";
import todos from "../features/todo/todoSlice";

const rootReducer = combineReducers({
  counter,
  todos
});

export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;

Store を修正

src/app/store.ts
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'
import rootReducer from "./rootReducer";

export const store = configureStore({
  reducer: rootReducer
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

各 Todo Components を修正して結合する

src/pages/todo/components/AddTodo.tsx
import React from "react";
import { useDispatch } from "react-redux"; // <-- 追記
import { addTodo } from "../../../features/todo/todoSlice"; // <-- 追記

export default function AddTodo(): JSX.Element {
  const dispatch = useDispatch(); // <-- 追記
  const [text, setText] = React.useState("");

  function handleChange(e: { target: HTMLInputElement }) {
    setText(e.target.value);
  }

  function handleSubmit(e: any) {
    e.preventDefault();

    if (!text.trim()) {
      return;
    }
    dispatch(addTodo(text)); // <-- 追記

    setText("");
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={handleChange} />
      <button type="submit">Add Todo</button>
    </form>
  );
}

src/pages/todo/components/TodoList.tsx
import * as React from "react";
import { useSelector } from "react-redux"; // <-- 追記
import { RootState } from "../../../app/rootReducer" // <-- 追記
import { Todo } from "../../../features/todo/types"; // <-- 追記
import TodoItem from './TodoItem'

//export default function TodoList() { <-- 削除
  // const text: any = ""; <-- 削除

export default function TodoList() { // <-- 追記
  const todos: Todo[] = useSelector((state: RootState) => // <-- 追記
    state.todos // <-- 追記
  ); // <-- 追記
  return (
    <ul>
      {/* --- 追記 ---  */}
      {(todos || []).map(todo => (
        <TodoItem {...todo} />
      ))}
      {/* --- ここまで --- */}
      {/* <TodoItem {...text} /> <-- 削除 */}
    </ul>
  );
}

起動して動作確認

qiita_react.gif

最後に

いかがでしたでしょうか、慣れましたか?
こちらのサイトがとても分かりやすく解説してくれています。感謝(-人-)
おそらく、この後にこちらをみると理解が深まると思います。

皆様の助けになると幸いです。
チュートリアル通りだと、その2以降は、タスクを完了させたり、タスクの完了・未完了のフィルタリングですが、そのうち投稿したいとおもうのですが、もう皆さんなら出来ると思うのです。
断じて力尽きたわけではありません。私もこれから勉強していきます(><)

今回のソースはこちらに置いておきます

でわ :wave:

1
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
6