React Hooks の useEffect の正しい使い方

React Hooks を使っている人なら必ず使う`useEffect`。`useEffect`はとても扱いが難しく、深く考えずに使ってしまうと思わぬバグを生んでしまったり、コードの変更を行うのが難くなってしまう。
本記事は、上記のような扱いの難しい`useEffect`の使い方をさっとみることができるように短くまとめたものである。

この記事は主に以下の2つの記事を参考に構成されている。

- [React Hooks Document FAQ](https://ja.reactjs.org/docs/hooks-faq.html#performance-optimizations)
- [A Complete Guide to useEffect - Overreacted](https://overreacted.io/a-complete-guide-to-useeffect/)

# 基本的な使い方

```jsx:Counter.js

const Counter = () => {
	const [ count, setCount ] = useState(0);

	/* 
  * useEffectは、第一引数にcallbackを入れて、第二引数に依存する値の配列を入れる
  * 依存する値が変更される度にcallbackが実行される
  */
	useEffect(
		() => {
			console.log(count);
		},
		[ count ]
	);

	return (
		<div>
			<p>{count}</p>
			<button onClick={() => setCount(count + 1)}>カウント</button>
		</div>
	);
};


```

# propsやstateに依存する関数を使用する

関数を`useEffect`で使う場合は、注意深く実装する必要がある。

### 悪い例

```jsx:Counter.js

const Counter = () => {
	const [ count, setCount ] = useState(0);

	// 偶数の時にlogを出力したい
	const outputEvenNumber = () => {
		if (count % 2 === 0) {
			console.log(count);
		}
	};

	useEffect(
		() => {
			outputEvenNumber();
		},
		[ outputEvenNumber ]
	);

	return (
		<div>
			<p>{count}</p>
			<button onClick={() => setCount(count + 1)}>カウント</button>
		</div>
	);
};

```

### この例のよくない点

- 現段階では`count`が更新される度に`outputEvenNumber`が再生成されているため正常に動いている。
- しかし、`useEffect`の第2引数に`outputEvenNumber`を入れていることで、もし`outputEvenNumber`に新しく**Componentのスコープ内にある値**を追加したいとなった時に正しく動かない可能性がある。
- つまりここで、`useEffect`に依存しているのは`outputEvenNumber`と`count`である。

上記の問題を解決するためには以下のように書く。

### 良い例

```jsx:Counter.js

const Counter = () => {
	const [ count, setCount ] = useState(0);

	useEffect(
		() => {
			// 偶数の時にlogを出力したい
			const outputEvenNumber = () => {
				if (count % 2 === 0) {
					console.log(count);
				}
			};
			outputEvenNumber();
		},
		[ count ]
	);

	return (
		<div>
			<p>{count}</p>
			<button onClick={() => setCount(count + 1)}>カウント</button>
		</div>
	);
};

```

### 改善した点

- 関数を`useEffect`の中に入れて,外部から依存している変数を`count`のみにした。
- こうすることで、`outputEvenNumber`に新しく**Componentのスコープ内にある値**を追加した時に依存関係が明確になる
- さらに、`outputEvenNumber`を`useEffect`の中に書くことで、関数が`useEffect`の第２引数に指定されている値以外の影響を受けていないことを保証できる。

# propsやstateに依存する関数をComponent内で共通で使用する

### 良い例 

```jsx:Counter.js

const Counter = () => {
	const [ count, setCount ] = useState(0);

	/*
  * useCallback は第2引数に指定した値が変化した時にのみ関数を再生成するHookです
  */
	const outputEvenNumber = useCallback(
		() => {
			if (count % 2 === 0) {
				console.log(count);
			}
		},
		[ count ]
	);

	useEffect(
		() => {
			outputEvenNumber();
			// Do something
		},
		[ outputEvenNumber ]
	);

	useEffect(
		() => {
			outputEvenNumber();
			// Do something
		},
		[ outputEvenNumber ]
	);

	return (
		<div>
			<p>{count}</p>
			<button onClick={() => setCount(count + 1)}>カウント</button>
		</div>
	);
};


```

### 良い点

- `useCallback`を使って`count`が依存していることを明確にしている
- 他の変数を加えた時に、`useCallback`の第２引数に値を追加すれば良いだけなので安全である

# propsやstateに依存しない関数を使用する

### 良い例 

```jsx:Counter.js

const outputEvenNumber = (count) => {
	if (count % 2 === 0) {
		console.log(count);
	}
};

const Counter = () => {
	const [ count, setCount ] = useState(0);

	useEffect(
		() => {
			outputEvenNumber(count);
			// Do something
		},
		[ count ]
	);

	return (
		<div>
			<p>{count}</p>
			<button onClick={() => setCount(count + 1)}>カウント</button>
		</div>
	);
};

```

### 良い点

- この場合は単純にComponentの外に定義してあげれば良い
- 中に定義するとしたら`useCallback`を使って第2引数に`[]`を指定すれば良いが、外に出した方が無駄な処理も減り、読みやすくなる

# useEffectの中でstateを更新したいとき 1

カウントアップをするようなUIを実現したい時を例にあげる

### 悪い例 1

```jsx:Counter.js

const Counter = () => {
	const [ count, setCount ] = useState(0);

	useEffect(
		() => {
			const time = setInterval(() => {
				setCount(count + 1);
			}, 1000);

			return () => clearInterval(time);
		},
		[ count, setCount ]
	);

	return (
		<div>
			<p>{count}</p>
		</div>
	);
};


```

### 悪い点

- stateが更新される度に`setInterval`を呼び出してしまっている

### 悪い例 2

```jsx:Counter.js

const Counter = () => {
	const [ count, setCount ] = useState(0);

	useEffect(() => {
		const time = setInterval(() => {
			setCount(count + 1);
		}, 1000);

		return () => clearInterval(time);
	},[]);

	return (
		<div>
			<p>{count}</p>
		</div>
	);
};


```

### 悪い点

- `count`変数が変化しないために、カウントアップされない
- `useEffect`内の`count`変数が更新されていないため、常に`count === 0`である
- そのため、常に`1`がレンダリングされる

### 良い例

```jsx:Counter.js

const Counter = () => {
	const [ count, setCount ] = useState(0);

	useEffect(() => {
		const time = setInterval(() => {
			setCount(prevCount => prevCount + 1);
		}, 1000);

		return () => clearInterval(time);
	}, []);

	return (
		<div>
			<p>{count}</p>
		</div>
	);
};

```

### 良い点

- 新しいstateが`setCount`関数に、引数として渡ってきているため、カウントアップに成功する

# useEffectの中でstateを更新したいとき 2

例えば、他の変数によって変化をつけたいときはどのようにすれば良いだろうか
次の例では秒数を指定する方法を書いた

### 良い例

```jsx:Counter.js

const reducer = (state, action) => {
	switch (action.type) {
		case "SET_TICK": {
			return {
               ...state,
               count: 0,
               tick: action.tick
			};
		}
		case "ADD_COUNT": {
			return {
				...state,
				count: state.count + 1
			};
		}
		default:
			return state;
	}
};

const Counter = () => {
	const [ state, dispatch ] = useReducer(reducer, { tick: 1000, count: 0 });

	const handleOnChangeTick = (e) => {
		dispatch({ type: "SET_TICK", tick: parseInt(e.target.value) });
	};

	useEffect(
		() => {
			const time = setInterval(() => {
				dispatch({ type: "ADD_COUNT" });
			}, state.tick);

			return () => clearInterval(time);
		},
		[ state.tick ]
	);

	return (
		<div>
			<p>{state.count}</p>
			<input value={state.tick} onChange={handleOnChangeTick} />
		</div>
	);
}

```

### 良い点

- `useReducer`を使うことで、複数の変数に対しても無駄な変数を`useEffect`の第２引数に入れることなく、実現できた
- また、`dispatch`関数は常に最新のstateが共有されるので`reducer`を見れば依存関係がすぐにわかる
- propsに依存する場合は、`reducer`関数をComponentの中で宣言するようにすれば良い

## バグを生まないために

バグを生まないために大切なのは静的にコードを解析して、バグの温床となるコードを予め潰すことです。これを行ってくれる一般的なツールが [ESLint](https://eslint.org/) です。幸い、[eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) という React の Hooks でも Lint を行ってくれる Plugin がすでに用意されているのでそれを使うとよりバグを減らすことができます。

この Plugin は `useEffect` の第2引数に指定している変数の数が足りていない時にエラーを出してくれるものです。

しかし、このプラグインも完璧ではなく、この Lint の指示通りに修正したとしてもバグが起こることもあります。そのため、とりあえずは Lint に従い、意図した挙動になっていない時にはコメントで`// eslint-disable-next-line react-hooks/exhaustive-deps`のように書くと、コメントの次の行のコードの Lint を無効にしてくれます。(ESLintのコメントは他にもあるので詳しくは[公式ドキュメント](https://eslint.org/)を参照してください)

以上です！間違っている点や、アドバイスなどがございましたら、コメントにて指摘していただければ幸いです。
ありがとうございました。