はじめに
最近 React を勉強し始めた新参者です。
「とりあえず TypeScript で始めよう → Redux って便利そう → Redux Toolkit ってのがあるのか~ ・・・・・・・・・ 」
という具合にインターネットを潜ってチュートリアル始めたら、情報が多かったり古かったりEnglishだったりでカオスったので、習うより慣れるチュートリアルをここに残します。
習うより慣れたい皆様の参考になれば幸いです。
環境
% 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
Start!!
プロジェクト作成
Redux+TypeScript テンプレートを使用
こちら
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
以下の構成が出来上がります
ディレクトリ構成
% 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
└── # 省略
既存のソースをちょっと修正する
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './pages/App'; // <-- 修正
// 以下省略
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>
// 以下省略
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アプリのベースを作成
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 を作成
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 毎に読み込むように修正
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 を修正
.App-logo {
height: 20vmin; /* 40 --> 20 */
/* 省略 */
}
.App-header {
min-height: 10vh; /* 100 --> 10 */
/* 省略 */
}
起動して見た目を確認
yarn run start
localhost:3000/
にアクセスしたときの見た目 (サンプルソース)
localhost:3000/todo
にアクセスしたときの見た目
Todoを追加できるようにする
Todoリストの componet を作成
- Todoリスト
import * as React from "react";
import TodoItem from './TodoItem'
export default function TodoList() {
const text: any = ""
return (
<ul>
<TodoItem {...text} />
</ul>
);
}
- Todo単体
import * as React from "react";
interface TodoProps {
text: string;
}
export default function TodoItem( { text }: TodoProps ) {
return (
<li>
{text}
</li>
);
}
- 作成した Todo Components を読み込む
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>
);
};
起動して見た目を確認
Todoリストの表示部分ができました
入力した値をTodoリストに出力する
Slice を作成
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 を作成
export interface Todo {
id: string;
text: string;
}
Reducer を作成
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 を修正
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 を修正して結合する
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>
);
}
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>
);
}
起動して動作確認
最後に
いかがでしたでしょうか、慣れましたか?
こちらのサイトがとても分かりやすく解説してくれています。感謝(-人-)
おそらく、この後にこちらをみると理解が深まると思います。
皆様の助けになると幸いです。
チュートリアル通りだと、その2以降は、タスクを完了させたり、タスクの完了・未完了のフィルタリングですが、そのうち投稿したいとおもうのですが、もう皆さんなら出来ると思うのです。
断じて力尽きたわけではありません。私もこれから勉強していきます(><)
今回のソースはこちらに置いておきます
でわ