LoginSignup
2
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-03

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

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

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書いた

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

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>
  );
}
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1