Engineering from Scratch

エンジニア目指してます

2022/07/03

You Might Not Need an Effect

Resetting all state when a prop changes

悪い例

import { useEffect, useState } from "react";

const App = () => {
  const [userId, setUserId] = useState(0);

  return (
    <>
      <button onClick={() => setUserId((userId) => userId + 1)}>+1</button>
      <Comment userId={userId} />
    </>
  );
};

type CommentProps = {
  userId: number,
};

const Comment = (props: CommentProps) => {
  const { userId } = props;
  const [comment, setComment] = useState("");

  useEffect(() => setComment(""), [userId]);

  return (
    <>
      <div>{userId}</div>
      <input value={comment} onChange={(e) => setComment(e.target.value)} />
      <div>{comment}</div>
    </>
  );
};

export default App;

userIdごとにコメントを出し分けるCommentコンポーネントが存在する。userIdが変化した時に,commentstate を初期化するために,Effect を使用している。

userIdstate が更新された時に,Commentコンポーネントが古い値のまま再レンダリングされ,その後 useEffect により,もう一度レンダリング処理が走るため,非効率。

良い例

import { useState } from "react";

const App = () => {
  const [userId, setUserId] = useState(0);

  return (
    <>
      <button onClick={() => setUserId((userId) => userId + 1)}>+1</button>
      <Comment userId={userId} key={userId} />
    </>
  );
};

type CommentProps = {
  userId: number,
};

const Comment = (props: CommentProps) => {
  const { userId } = props;
  const [comment, setComment] = useState("");

  return (
    <>
      <div>{userId}</div>
      <input value={comment} onChange={(e) => setComment(e.target.value)} />
      <div>{comment}</div>
    </>
  );
};

export default App;

CommentコンポーネントuserIdを key として持たせることで解決できる。key が違うと別のコンポーネントとみなされるため,userIdが変化する度に,commentstate は初期化される。

Initializing the application

初期マウント時に一度だけ行いたい処理に useEffect を使用した時,開発環境では2回レンダリングが起こる。ログイン処理など,一度だけ行われるべき処理では,以下のように対処すべき。

import { useEffect } from "react";

let didInit = false;

const App = () => {
  useEffect(() => {
    if (didInit) return;

    didInit = true;
    console.log("Hello world");
  }, []);

  return (
    <>
      <h1>Hello World</h1>
    </>
  );
};

export default App;

Subscribing to an external store

外部から取得したデータについて考える。

悪い例

import { useEffect, useState } from "react";

const useOnlineStatus = () => {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    const updateState = () => {
      setIsOnline(navigator.onLine);
    };

    updateState();

    window.addEventListener("online", updateState);
    window.addEventListener("offline", updateState);
    return () => {
      window.removeEventListener("online", updateState);
      window.removeEventListener("offline", updateState);
    };
  }, []);

  return isOnline;
};

const App = () => {
  const isOnline = useOnlineStatus();

  if (isOnline) {
    console.log("Online!");
  } else {
    console.log("Offline!");
  }

  return <h1>Hello World</h1>;
};

export default App;

navigator.onLineというブラウザのオンライン状態を返すために Effect を使用している。

良い例

useSyncExternalStoreという React 独自の hook を使用する。

import { useSyncExternalStore } from "react";

const subscribe = (callback: () => void) => {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
};

const useOnlineStatus = () => {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine,
    () => true
  );
};

const App = () => {
  const isOnline = useOnlineStatus();

  if (isOnline) {
    console.log("Online!");
  } else {
    console.log("Offline!");
  }

  return <h1>Hello World</h1>;
};

export default App;

参考

beta.reactjs.org