クラスで書いたReactコンポーネントをHooksで書いてみる


この記事は「[株式会社オープンストリーム "小ネタ" Advent Calendar 2020](https://qiita.com/advent-calendar/2020/opst-mini)」の18日目の記事です。

## はじめに

Reactを学習して次はHooksを導入しよう…と少しハマったので、「どの手順で導入するか」「ハマった点」をまとめました。

## 環境

```bash
$ node --version
v14.15.0
```

```json:packages.json
{
...
  "dependencies": {
    "next": "10.0.3",
    "react": "17.0.1",
    "react-dom": "17.0.1"
  },
  "devDependencies": {
    "@types/node": "^14.14.10",
    "@types/react": "^17.0.0",
    "typescript": "^4.1.2"
  }
}

```

次のクラスコンポーネントで書いたReactのTSXを使います。[元ネタはこちらです](https://www.to-r.net/media/react-tutorial13/)

```tsx:src/App.tsx
import styles from "../styles/Home.module.css";
import todoStyles from "../styles/Todo.module.css";
import React, { Component } from "react";

type Props = {};

type State = {
  todo: TodoItem[];
  currentText: string;
};

type TodoItem = {
  title: String;
};

class App extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {
      todo: [
        { title: "JavaScript覚える" },
        { title: "jQuery覚える" },
        { title: "ES2015覚える" },
        { title: "React覚える" },
      ],
      currentText: "",
    };

    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    this.setState({
      currentText: event.target.value,
    });
  }
  
  addTodo =()=> {
    this.state.todo.push({
      title: this.state.currentText
    })
    this.setState({
      todo: this.state.todo,
      currentText: ''+
    })
  }
  
  deleteTodo = (i: number)=> {
    this.state.todo.splice(i, 1)
    this.setState({
      todo: this.state.todo
    })
  }
  
  render() {
    return (
      <div>
        <h1>TODOアプリ</h1>
        <nav>
        <ul>{this.state.todo.map( (todo, i) => {
          return <li key={i}><input type="button" value="☓" onClick={() => this.deleteTodo(i)}/> {todo.title}</li>
        })}</ul>
        <input type="text" value={this.state.currentText} onChange={this.handleChange}/> <input type="button" value="追加" onClick={this.addTodo} />
      </div>
    );
  }
```


## `useState` からHooksをはじめる

次のステップでクラスで書いたコンポーネントをHooks使用した操作に書き換えることができます。

1. クラスコンポーネントから関数コンポーネントにする
2. Reactで使いたいStateを `useState` で宣言する

関数コンポーネントはアロー関数でも書くことができます。

```tsx:関数コンポーネント
export default function Home() {
  ...
};
```

```tsx:関数コンポーネント(アロー関数)
const Home = () => {
  ...
};
  
export default Home;
```

次に、Stateを `useState` で宣言します。 `useState` をimportした後に **関数コンポーネントの中で** 次のように記述します。

```tsx:useStateの例
import React, { useState } from "react";

const Home = () => {
  const [todoList, setTodoList] = useState(initialItems);
};
  
export default Home;
```

 * `todoList`: 管理したいStateの名前
 * `setTodoList`: Stateをセットするために新しく作る関数名
 * `initialItems`: Stateの初期値

[Reactのドキュメントでは](https://ja.reactjs.org/docs/hooks-intro.html) プリミティブで簡単な形を初期値として与えていますが、Objectがたくさん入ったArrayでも対応します。
TypeScriptで独自の型を定義しても動作します。

```tsx:少し複雑な初期値をuseStateに与えた例
import React, { useState } from "react";

type TodoItem = {
  title: String;
};

const Home = () => {
  const initialItems: TodoItem[] = [
    { title: "JavaScript覚える" },
    { title: "jQuery覚える" },
    { title: "ES2015覚える" },
    { title: "React覚える" },
  ];

  const [todoList, setTodoList] = useState(initialItems);

  // Reqctのドキュメントに載っているプリミティブな例
  const [count, setCount] = useState(0);

  ...
```

## 値の読み取り

クラスコンポーネントは `this.state.hoge` でStateの値を使うことができますが、Hooks(関数コンポーネント) になると `this.state` を取り除いて使うことができます。

リストのStateを繰り返し処理する例でも `this.state` を取り除いて使います。

```tsx:クラスコンポーネントで値を読み取る例
{this.state.todoList.map((todo, i) => {
  return (
    ...
  );
 })
}
```

```tsx:Hooks(関数コンポーネント)で値を読み取る例
{todoList.map((todo, i) => {
  return (
    ...
  );
 })
}
```

## 値のセット

クラスコンポーネントは `this.state.hoge` でStateを操作していましたが、Hooks(関数コンポーネント) では `useState` で定義したセット用の関数を用います。
例えば、先ほど定義した操作用関数を `onClick` に仕込むと次のようになります。

```tsx:Hooks(関数コンポーネント)で操作用関数を使って値をセットする例
<input type="button" value="全消去" onClick={() => setTodoList([])} />
```

ただし、`Array.splice()` など配列を操作する場合はこの方法だとうまくいきません。


### Hooksで扱うStateは `Array.splice()` などの変更が取れない

Hooksで提供されているState更新用関数では、操作によって値が異なった場合に画面を再描画して反映させています。

この値が異なっているかどうかの判定( `Object.is` )の仕様により、配列そのものを操作する場合は **別の配列にコピーしてそれを操作、setする方法などで変更後のものは別のオブジェクトであると認識せる必要があります。** 

「Stateで使っている配列を操作して画面に反映させたい」といったことを考える場合は留意するポイントです。

```tsx:配列そのものを変更する例
const Home = () => {
  const deleteTodo = (index: number) => {
    const newTodoList = [...todoList];
    newTodoList.splice(index, 1);
    setTodoList(newTodoList);
  };

  return (
    ...
    <input type="button" value="上から2番目を消す" onClick={() => deleteTodo(1)} />
  );
};

export default Home;
```

次のように同じ配列に対して操作してsetしても、データ上では反映されますが再描画が発生せず正しく画面に反映されません(他のStateを操作すると反映される場合があります)

```tsx:正しく反映されない例
  const deleteTodo = (index: number) => {
    todoList.splice(index, 1);
    setTodoList(todoList);
  };
```


## 参考

(コードの元ネタ)
React で作る TODO アプリ前編 – React 入門 - to-R Media
https://www.to-r.net/media/react-tutorial13/

フックの導入 – React
https://ja.reactjs.org/docs/hooks-intro.html

5分でわかるReact Hooks - Qiita
https://qiita.com/Mitsuzara/items/98d1bc4a83265a764084

React Hooksで配列の要素を削除したあとの再描画について
https://www.ravness.com/posts/reactsplice

フック API リファレンス – React (state 更新の回避)
https://ja.reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update