関数コンポネントのライフサイクル使うhook

## 関数コンポーネントのライフサイクル

Reactの関数コンポーネントにも、mount / unmount の概念が存在します。

useState など (公式の) hooksの戻り値は、react内部の配列のような構造で管理・配分された値です。もし本当にunmountされた後で使うと、Reactのdebug buildは↓のように警告してくれます。production版は試していないが、もし問題になると、このようなミスを探すのが難しいと予想します。

```
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
```

## async/await とリソース解放

無駄な計算とメモリリークを防止するために、コンポーネントによって配分された各種リソース (開始した非同期タスクなど) は、unmount以降早く解放すべき。

effectの中で割り当てられて、`dispose()` などAPIを設けてくれたリソースならいいが、async/awaitの処理フローはそうではなく、Promiseオブジェクトが生成された時点からcancelもpauseもできない。

さらに、1箇所にawait使うと、伝染のように以降の処理のタイミング (とリソース配分) もpromiseに依存するようになる。

async/awaitを使わないで、非同期処理のフローをuseEffect()に合わせることはできなくはないが、awaitの回数によってロジック上連続する処理を複数のuseEffectに分けるようになってしまうため、私はそう書きたくない。

## あるコード例

こんなコンポーネント作ることとします:

- あるボタンをクリックすると、signIn() API でログインして (このAPIはPromiseを返す)、返されたユーザ名をUIに表示する
- ↑ signIn() Promiseがfulfillした時点からの秒数をUIに表示する

## version 1

```ts
const SignInTimerV1: React.FC = () => {
  const [username, setUsername] = useState(null);
  const [timeSinceLogin, setTimeSinceLogin] = useState(NaN);

  const onClick = async () => {
    const user = await signIn();

    setUsername(user.name);
    const timer = setInterval(() => {
      setTimeSinceLogin(t => t+1000);
    }, 1000);
  };

  return (
    <div>
      <button onClick={onClick} > signIn() </button>
      <p>current user: { username } </p>
      <p>time since sign in: {timeSinceLogin} </p>
    </div>
  );
}
```

もしsignIn() がfulfillした時点で すでにunmountされてると、setUsernameを呼ぶべきではない。そして、unmountされるときtimerを解放しないとメモリリークになる。

## こんなhooks書いた

```js
export function useLifeCycle() {
  const l = useMemo(() => {
    const mounted /* boolean */ = false;
    const unmountCallback /* Function[] */ = [];
    const mountCallback /* Function[] */ = [];
    const unmounted /* Promise<void> */ = new Promise(fulfill => unmountCallback.push(fulfill));
    return {
      mounted,
      unmountCallback,
      mountCallback,
      unmounted,
    };
  }, []);

  useEffect(() => {
    l.mounted = true;
    for (const f of l.mountCallback) f();
    return () => {
      for (const f of l.unmountCallback) f();
      l.mounted = false;
    };
  }, []);

  return l;
}
```

- 呼び出し元のコンポーネントのライフサイクル状態 (mounted) を返す
- classコンポーネントのように、mount時点 (componentDidMount)、unmount時点 (componentWillUnmount) にコールバックを登録できる

## version 2

```ts
const SignInTimerV2: React.FC = () => {
  const lifecycle = useLifeCycle();                               // changed
  const [username, setUsername] = useState(null);
  const [timeSinceLogin, setTimeSinceLogin] = useState(NaN);

  const onClick = async () => {
    const user = await signIn();

    if (!lifecycle.mounted) return;                               // changed
    setUsername(user.name);
    const timer = setInterval(() => {
      if (lifecycle.mounted) setTimeSinceLogin(t => t+1000);      // changed
    }, 1000);

    lifecycle.unmounted.then(() => clearInterval(timer));         // changed
  };

  return (
    <div>
      <button onClick={onClick} > signIn() </button>
      <p>current user: { username } </p>
      <p>time since sign in: {timeSinceLogin} </p>
    </div>
  );
}
```