ReactのHooksを使ってReduxのTodoリストをReduxなしに書き換えたサンプル(TypeScript使用)

# 注意

わたしは趣味でreactを勉強しているものです。間違いなどありましたらご指摘お願いしますm(_ _)m

# 参考

- [【React】新機能hooks - Qiita](https://qiita.com/YutamaKotaro/items/ef0430f570779dcf8a26)
- [ReactでcreateContextするときにTypeScriptのリントがエラーを吐く場合の対処法 - Qiita](https://qiita.com/Statham/items/1567055575acb4421f23)
- [Hooks API Reference – React](https://reactjs.org/docs/hooks-reference.html)
- [Context – React](https://reactjs.org/docs/context.html)
- [Using Typescript with modern React (i.e. hooks, context, suspense) - youtube](https://www.youtube.com/watch?v=BnIhk4igd8I)
- [tate Management with React Hooks and Context API in 10 lines of code! - medium](https://medium.com/simply/state-management-with-react-hooks-and-context-api-at-10-lines-of-code-baf6be8302c)

# ReduxのTodoリスト

こちらのサンプルをReactのHooksに書き換えてみます。

- [Example: Todo List · Redux](https://redux.js.org/basics/example)
- [CodeSandbox](https://codesandbox.io/s/github/reduxjs/redux/tree/master/examples/todos)

【[私がReactのHooksに書き換えてみたコードはこちら](https://github.com/okumurakengo/react-hooks-todos)】

# バージョン

- 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

<details><summary>jsで`useState`のサンプル</summary><div>

```jsx
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"));
```

1. `useState("")` を呼び出します、引数に設定した値が初期値になります
2. `[text, setText] = useState("");` で`text`, `setText`を受け取ります。`text`は初期値を空文字に設定したので、この時点では空文字が設定されています。
3. `setText("hogefuga")` と実行すると、`text`の値が`hogefuga`と変わります。


![s3hyuG00xE.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/bc586b90-465e-319d-2143-6b35f238e71d.gif)

</div></details>

##### ts

<details><summary>tsで`useState`のサンプル</summary><div>

```tsx
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"));
```

![5zPwrMT1Ny.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/6f738d91-e3a2-9bc3-98bd-664d4a603a54.gif)

```ts
  const [text, setText] = React.useState<string>("");
  const [items, setItems] = React.useState<IItem[]>([]);
```

`useState<型>(初期値)` とすることで型を指定します。

`setState`を使うときに指定の型ではなかった場合はエラーにすることができます。

![Screen Shot 2019-06-23 at 19.27.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/be952812-bafb-1b0f-faa6-de88f8d46a24.png)

</div></details>

### 1-2. `useContext`

##### `useContext`の前に`createContext`を復習

<details><summary>jsで`createContext`のサンプル</summary><div>

以下の例は、`App` -> `Toolbar` -> `ThemedButton` で入れ子になっているコンポーネントです。
`props`でバケツリレーすることなく、`App` -> `ThemedButton`と値を受け渡せます。

```jsx
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"));
```

</div></details>

##### js

<details><summary>jsで`useContext`のサンプル</summary><div>

↑の`createContext`のサンプルを`useContext`から値を取得するように変えてみた例です。

```jsx
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"));
```

</div></details>

##### ts

<details><summary>tsで`useContext`のサンプル</summary><div>

```tsx
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`を使うとうまくいくようです。

```ts
// Partial<IContext>とすると、このように宣言したのと同じように扱ってくれる
interface IContext {
  theme?: string;
  color?: string;
  font?: string;
}
```

</div></details>

### 1-3. `useReducer`

##### js

<details><summary>jsで`useReducer`のサンプル</summary><div>

公式のサンプルです。

https://reactjs.org/docs/hooks-reference.html#usereducer

```jsx
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"));
```

1. `useReducer(reducer, initialState);` で`reducer`とstateの初期値を渡します。
    - `dispatch`はstateを更新するときに呼び出す関数です。dispatchの引数は、`dispatch({ type: "decrement", payload: hoge })}` とするのが一般的なようです。
1.  `dispatch({ type: "decrement" })` とすると、`reducer`が実行され、stateを変更してくれます。

</div></details>

##### ts

<details><summary>tsで`useReducer`のサンプル</summary><div>

```tsx
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`のそれぞれ型指定してみました。

</div></details>


# 2. ReactとTypeScriptの環境をwebpackで作る

ここからReactのHooksでTodoリストを作成してみます

### 2-1. インストール

```bash
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`

```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`

今回はこのような設定にしてみました。

```js: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. フォルダ構成

```bash
.
├── 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`

```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`を受け取れます。

```tsx
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リストの大枠のコンポーネント

```tsx
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箇所しか使わないような型定義は、それぞれのファイルにそのまま書いてます。

```ts
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をこの関数で管理しています。

```ts
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`を使いまわします。

```tsx
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>
  );
};
```

```tsx
// 悩みどころ
// ここで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のリストに追加する処理です。

```tsx
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`の値によってリストをフィルターします。

```tsx
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が更新されます。

```tsx
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`

フィルターのボタンを配置している部分です。

```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;
```

```jsx
// このように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が更新されます。

```tsx
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;
```

---

![yzYFmq6hR3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/3b6ebe9f-f757-ec11-5675-1bee8935f0d5.gif)

ReactのHooksを使ってTodoリストを動かすことができました。最後まで読んでいただいてありがとうございましたm(_ _)m

# この記事を書くのに使ったライブラリ、ツールなど

今回のサンプルで使用させていただいたライブラリなどは以下です。

よいと思った方はgithubで:star2:をつけるとよい思います。

- [facebook/react - github](https://github.com/facebook/react)
- [microsoft/TypeScript - github](https://github.com/microsoft/TypeScript)
- [webpack/webpack - github](https://github.com/webpack/webpack)
- [webpack/webpack-cli - github](https://github.com/webpack/webpack-cli)
- [webpack/webpack-dev-server - github](https://github.com/webpack/webpack-dev-server)
- [TypeStrong/ts-loader](https://github.com/TypeStrong/ts-loader)

---

この記事の文章は`textlint`、`textlint-rule-preset-japanese`を使ってLintさせていただきました。m(_ _)m

良いと思った方はgithubで:star2:をつけるとよい思います。

- [textlint/textlint - github](https://github.com/textlint/textlint)
- [textlint-ja/textlint-rule-preset-japanese - github](https://github.com/textlint-ja/textlint-rule-preset-japanese)