注意
わたしは趣味でreactを勉強しているものです。間違いなどありましたらご指摘お願いしますm(_ _)m
参考
- 【React】新機能hooks - Qiita
- ReactでcreateContextするときにTypeScriptのリントがエラーを吐く場合の対処法 - Qiita
- Hooks API Reference – React
- Context – React
- Using Typescript with modern React (i.e. hooks, context, suspense) - youtube
- tate Management with React Hooks and Context API in 10 lines of code! - medium
ReduxのTodoリスト
こちらのサンプルをReactのHooksに書き換えてみます。
【私がReactのHooksに書き換えてみたコードはこちら】
バージョン
- react : 16.8.6
- typescript : 3.5.2
- webpack : 4.35.0
1. Hooksの基本を復習
最初にHooksの使い方をおさらいすると理解が深まりました。知っている方は飛ばして大丈夫です。
Hooksは他にもありますが今回使ったHooksは以下3つなので、この3つを復習します。
useState
useContext
useReducer
1-1. useState
js
jsで`useState`のサンプル
import React, { useState } from "react";
import { render } from "react-dom";
const App = () => {
const [text, setText] = useState("");
return (
<>
<p>text: {text.split("").reverse().join("")}</p>
<input value={text} onChange={e => setText(e.target.value)} />
</>
);
};
render(<App />, document.getElementById("root"));
-
useState("")
を呼び出します、引数に設定した値が初期値になります -
[text, setText] = useState("");
でtext
,setText
を受け取ります。text
は初期値を空文字に設定したので、この時点では空文字が設定されています。 -
setText("hogefuga")
と実行すると、text
の値がhogefuga
と変わります。
ts
tsで`useState`のサンプル
import * as React from "react";
import { render } from "react-dom";
interface IItem {
id: number;
name: string;
}
const App: React.FC = (): JSX.Element => {
const [text, setText] = React.useState<string>("");
const [items, setItems] = React.useState<IItem[]>([]);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setItems([...items, { id: items.length + 1, name: text }]);
setText("");
};
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={e => setText(e.target.value)} />
<button>Add</button>
{items.map((item: IItem) => <p>{item.name}</p>)}
</form>
);
};
render(<App />, document.getElementById("root"));
const [text, setText] = React.useState<string>("");
const [items, setItems] = React.useState<IItem[]>([]);
useState<型>(初期値)
とすることで型を指定します。
setState
を使うときに指定の型ではなかった場合はエラーにすることができます。
1-2. useContext
useContext
の前にcreateContext
を復習
jsで`createContext`のサンプル
以下の例は、App
-> Toolbar
-> ThemedButton
で入れ子になっているコンポーネントです。
props
でバケツリレーすることなく、App
-> ThemedButton
と値を受け渡せます。
import React, { createContext } from "react";
import { render } from "react-dom";
const { Provider, Consumer } = createContext();
const ThemedButton = ({ children }) => (
<Consumer>
{({ color, onClick }) => (
<button style={{ backgroundColor: color }} onClick={onClick}>
{children}
</button>
)}
</Consumer>
);
const Toolbar = () => (
<ThemedButton>buttonA</ThemedButton>
);
const App = () => (
<Provider value={{ color: "skyblue", onClick: () => alert("sample!") }}>
<Toolbar />
</Provider>
);
render(<App />, document.getElementById("root"));
js
jsで`useContext`のサンプル
↑のcreateContext
のサンプルをuseContext
から値を取得するように変えてみた例です。
import React, { createContext, useContext } from "react";
import { render } from "react-dom";
const ThemeContext = createContext();
const ThemedButton = ({ children }) => {
const { color, onClick } = useContext(ThemeContext);
return (
<button style={{ backgroundColor: color }} onClick={onClick}>
{children}
</button>
);
};
const Toolbar = () => <ThemedButton>buttonA</ThemedButton>;
const App = () => (
<ThemeContext.Provider
value={{ color: "skyblue", onClick: () => alert("sample!") }}
>
<Toolbar />
</ThemeContext.Provider>
);
render(<App />, document.getElementById("root"));
ts
tsで`useContext`のサンプル
import * as React from "react";
import { render } from "react-dom";
interface IContext {
theme: string;
color: string;
font: string;
}
const ThemeContext = React.createContext<Partial<IContext>>({});
const ThemedButton: React.FC = ({ children }): JSX.Element => {
const { color } = React.useContext(ThemeContext);
return <button style={{ backgroundColor: color }}>{children}</button>;
};
const Toolbar: React.FC = (): JSX.Element => (
<ThemedButton>buttonA</ThemedButton>
);
const App: React.FC = (): JSX.Element => (
<ThemeContext.Provider value={{ color: "skyblue" }}>
<Toolbar />
</ThemeContext.Provider>
);
render(<App />, document.getElementById("root"));
React.createContext<Partial<IContext>>({});
と型を指定しました。
React.createContext<IContext>({});
としてもいいのですが、初期値に値が一つもないとエラーになるようでした。
Parcial
を使うとうまくいくようです。
// Partial<IContext>とすると、このように宣言したのと同じように扱ってくれる
interface IContext {
theme?: string;
color?: string;
font?: string;
}
1-3. useReducer
js
jsで`useReducer`のサンプル
公式のサンプルです。
https://reactjs.org/docs/hooks-reference.html#usereducer
import React, { useReducer } from "react";
import { render } from "react-dom";
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</>
);
};
render(<Counter />, document.getElementById("root"));
-
useReducer(reducer, initialState);
でreducer
とstateの初期値を渡します。-
dispatch
はstateを更新するときに呼び出す関数です。dispatchの引数は、dispatch({ type: "decrement", payload: hoge })}
とするのが一般的なようです。
-
-
dispatch({ type: "decrement" })
とすると、reducer
が実行され、stateを変更してくれます。
ts
tsで`useReducer`のサンプル
import * as React from "react";
import { render } from "react-dom";
interface IState {
count: number;
}
interface IAction {
type: "increment" | "decrement";
}
const initialState: IState = { count: 0 };
function reducer(state: IState, action: IAction) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
const Counter: React.FC = (): JSX.Element => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</>
);
};
render(<Counter />, document.getElementById("root"));
state
とaction
のそれぞれ型指定してみました。
2. ReactとTypeScriptの環境をwebpackで作る
ここからReactのHooksでTodoリストを作成してみます
2-1. インストール
yarn init -y
yarn add react react-dom
yarn add -D webpack webpack-cli webpack-dev-server ts-loader typescript @types/react @types/react-dom
2-2. webpack.config.js
module.exports = {
mode: "development",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader"
}
]
},
resolve: {
extensions: [".ts", ".tsx", ".js"]
},
devServer: {
port: 9000,
contentBase: "./",
publicPath: "/dist/",
open: "Google Chrome"
}
};
2-3. tsconfig.json
今回はこのような設定にしてみました。
{
"compilerOptions": {
// コンパイル後の.jsのバージョンを指定
"target": "es5",
// モジュールをどの形式にするかを指定 "None", "CommonJS", "AMD", "System", "UMD", "ES6", "ES2015" "ESNext".
// "ESNext"は、サポートされている最新のESが提案する機能を対象。
// 今回は「typescriptで指定のモジュールに変換 -> tsで生成されたモジュールの形式でwebpackでバンドル」
// とするので、webpackが対応しているモジュールの形式ならどれ指定してもコンパイルできるようでした。
// 【参考】webpackが対応しているモジュール:https://webpack.js.org/concepts/modules/
"module": "esnext",
// 厳密な型チェックオプションをすべて有効にする
// trueにすると ↓ を全部trueにしたのと同じ意味
// --noImplicitAny, --noImplicitThis, --alwaysStrict, --strictBindCallApply, --strictNullChecks, --strictFunctionTypes and --strictPropertyInitialization
"strict": true,
// .jsファイルもコンパイルできるように
"allowJs": true,
// jsxを変換するか
// "preserve":jsxを変換しない「<div />」->「<div />」、jsxの変換はbabelで行いたい場合などで使う
// "react":jsxを変換する「<div />」->「React.createElement("div")」
// "react-native":jsxを変換しない「<div />」->「<div />」
"jsx": "react"
},
// コンパイルに含めるフォルダ
"include": ["src"]
}
2-4. フォルダ構成
.
├── index.html
├── package.json
├── src
│ ├── components
│ │ ├── AddTodo.tsx
│ │ ├── App.tsx
│ │ ├── FilterLink.tsx
│ │ ├── Footer.tsx
│ │ ├── Store.tsx
│ │ ├── Todo.tsx
│ │ └── TodoList.tsx
│ ├── index.tsx # webpackの起点のファイル
│ ├── interface.ts
│ └── reducer.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
2-5. Todoリスト作成
index.html
<!DOCTYPE html>
<meta charset="UTF-8">
<title>Document</title>
<script src="dist/main.js" defer></script>
<div id="root"></div>
src/index.tsx
Store.tsx
でcreateContext
としているので、
StoreProvider
の子コンポーネントのApp
とその入れ子のコンポーネントでuseContext
とすると、StoreProvider
からのcontext
を受け取れます。
import * as React from "react";
import { render } from "react-dom";
import App from "./components/App";
import { StoreProvider } from "./components/Store";
render(
<StoreProvider>
<App />
</StoreProvider>,
document.getElementById("root")
);
src/components/App.tsx
Todoリストの大枠のコンポーネント
import * as React from "react";
import AddTodo from "./AddTodo";
import TodoList from "./TodoList";
import Footer from "./Footer";
const App: React.FC = (): JSX.Element => {
return (
<>
<AddTodo />
<TodoList />
<Footer />
</>
);
};
export default App;
src/interface.ts
いろんなファイルで共通の型定義はinterface.ts
にまとめました。
ここ以外で、1箇所しか使わないような型定義は、それぞれのファイルにそのまま書いてます。
export interface ITodoState {
id: number;
text: string;
completed: boolean;
}
export type TFilters = "SHOW_ALL" | "SHOW_COMPLETED" | "SHOW_ACTIVE";
export interface IState {
todos: ITodoState[];
visibilityFilter: TFilters;
nextTodoId: number;
}
interface IAddTodoAction {
type: "ADD_TODO";
payload: { id: number; text: string };
}
interface IToggleTodoAction {
type: "TOGGLE_TODO";
payload: { id: number };
}
interface ISetVisibilityFilterAction {
type: "SET_VISIBILITY_FILTER";
payload: { filter: TFilters };
}
export type IActions =
| IAddTodoAction
| IToggleTodoAction
| ISetVisibilityFilterAction;
export interface IStoreProvider {
state: IState;
dispatch: React.Dispatch<IActions>;
}
src/reducer.ts
dispatch
を実行したときこの関数が実行され、新しいstateにする部分です。
reduxのcombineReducers
を使っていないので、全てのstateをこの関数で管理しています。
import { IState, IActions } from "./interface";
export const reducer = (state: IState, action: IActions): IState => {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [
...state.todos,
{
id: action.payload.id,
text: action.payload.text,
completed: false
}
],
nextTodoId: state.nextTodoId + 1
};
case "TOGGLE_TODO":
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case "SET_VISIBILITY_FILTER":
return {
...state,
visibilityFilter: action.payload.filter
};
default:
return state;
}
};
src/components/Store.tsx
ここでuseReducer
として、state
, dispatch
を作成。
Provider
のvalueに渡し、いろんなコンポーネントでuseContext
としてstate
, dispatch
を使いまわします。
import * as React from "react";
import { IState, IStoreProvider } from "../interface";
import { reducer } from "../reducer";
const initialState: IState = {
todos: [],
visibilityFilter: "SHOW_ALL",
nextTodoId: 0
};
export const Store = React.createContext<IState | any>(initialState);
export const StoreProvider: React.FC = ({ children }): JSX.Element => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<Store.Provider value={{ state, dispatch }}>{children}</Store.Provider>
);
};
// 悩みどころ
// ここでanyにせず、IStateのみの型指定にすると、
export const Store = React.createContext<IState | any>(initialState);
// ------- ↓↓↓↓↓ -------
export const Store = React.createContext<IState>(initialState);
// ここのStore.Providerで型エラーになってしまいます。
<Store.Provider value={{ state, dispatch }}>{children}</Store.Provider>
// createContextのジェネリクスでanyにせず良いやり方があれば教えていただきたいですm(_ _)m
src/components/AddTodo.tsx
Todoのリストに追加する処理です。
import * as React from "react";
import { Store } from "./Store";
import { IStoreProvider } from "../interface";
const AddTodo: React.FC = (): JSX.Element => {
const { state, dispatch }: IStoreProvider = React.useContext(Store);
const [value, setValue] = React.useState<string>("");
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!value.trim()) return;
dispatch({
type: "ADD_TODO",
payload: {
id: state.nextTodoId,
text: value.trim()
}
});
setValue("");
};
return (
<form onSubmit={handleSubmit}>
<input value={value} onChange={e => setValue(e.target.value)} />
<button type="submit">Add Todo</button>
</form>
);
};
export default AddTodo;
src/components/TodoList.tsx
Todoリストを表示する部分です。state.visibilityFilter
の値によってリストをフィルターします。
import * as React from "react";
import { Store } from "./Store";
import { IStoreProvider, ITodoState, TFilters } from "../interface";
import Todo from "./Todo";
const TodoList: React.FC = (): JSX.Element => {
const { state, dispatch }: IStoreProvider = React.useContext(Store);
const handleClick = (id: number) => {
dispatch({
type: "TOGGLE_TODO",
payload: { id }
});
};
const getVisibleTodos = (todos: ITodoState[], filter: TFilters) => {
switch (filter) {
case "SHOW_ALL":
return todos;
case "SHOW_COMPLETED":
return todos.filter((t: ITodoState) => t.completed);
case "SHOW_ACTIVE":
return todos.filter((t: ITodoState) => !t.completed);
default:
throw new Error("Unknown filter: " + filter);
}
};
return (
<ul>
{getVisibleTodos(state.todos, state.visibilityFilter).map(
(todo: ITodoState) => (
<Todo key={todo.id} {...todo} onClick={() => handleClick(todo.id)} />
)
)}
</ul>
);
};
export default TodoList;
src/components/Todo.tsx
todoの一つ一つのコンポーネントです。
TodoList.tsx
から渡されるonClick
が実行されると、dispatch
が実行されてstateが更新されます。
import * as React from "react";
interface ITodoProps {
completed: boolean;
text: string;
onClick: () => void;
}
const Todo: React.FC<ITodoProps> = ({
text,
completed,
onClick
}): JSX.Element => {
return (
<li
onClick={onClick}
style={{
textDecoration: completed ? "line-through" : "none"
}}
>
{text}
</li>
);
};
export default Todo;
src/components/Footer.tsx
フィルターのボタンを配置している部分です。
import * as React from "react";
import { Store } from "./Store";
import { IStoreProvider, TFilters } from "../interface";
import FilterLink from "./FilterLink";
const Footer: React.FC = (): JSX.Element => {
const { state, dispatch }: IStoreProvider = React.useContext(Store);
const handleClick = (filter: TFilters): void => {
dispatch({
type: "SET_VISIBILITY_FILTER",
payload: { filter }
});
};
const getIsActive = (filter: TFilters): boolean => {
return state.visibilityFilter === filter;
};
return (
<>
<span>Show:</span>
<FilterLink {...{ handleClick, getIsActive, filter: "SHOW_ALL" }}>
ALL
</FilterLink>
<FilterLink {...{ handleClick, getIsActive, filter: "SHOW_ACTIVE" }}>
Active
</FilterLink>
<FilterLink {...{ handleClick, getIsActive, filter: "SHOW_COMPLETED" }}>
Completed
</FilterLink>
</>
);
};
export default Footer;
// このようにpropsを渡している部分は、
<FilterLink {...{ handleClick, getIsActive, filter: "SHOW_ALL" }}>
ALL
</FilterLink>
// こういう風にpropsを渡すのと同じに意味になります
<FilterLink handleClick={handleClick} getIsActive={getIsActive} filter="SHOW_ALL">
ALL
</FilterLink>
src/components/FilterLink.tsx
フィルターをかけるボタンの部分。
TodoList.tsxから渡されるhandleClickが実行されると、dispatchが実行されてstateが更新されます。
import * as React from "react";
import { TFilters } from "../interface";
interface ILinkProps {
handleClick: (filter: TFilters) => void;
getIsActive: (filter: TFilters) => boolean;
filter: TFilters;
}
const Link: React.FC<ILinkProps> = ({
children,
handleClick,
getIsActive,
filter
}): JSX.Element => {
return (
<button onClick={() => handleClick(filter)} disabled={getIsActive(filter)}>
{children}
</button>
);
};
export default Link;
ReactのHooksを使ってTodoリストを動かすことができました。最後まで読んでいただいてありがとうございましたm(_ _)m
この記事を書くのに使ったライブラリ、ツールなど
今回のサンプルで使用させていただいたライブラリなどは以下です。
よいと思った方はgithubでをつけるとよい思います。
- facebook/react - github
- microsoft/TypeScript - github
- webpack/webpack - github
- webpack/webpack-cli - github
- webpack/webpack-dev-server - github
- TypeStrong/ts-loader
この記事の文章はtextlint
、textlint-rule-preset-japanese
を使ってLintさせていただきました。m(_ _)m
良いと思った方はgithubでをつけるとよい思います。