1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Reactについての知見まとめ

Posted at

useStateの状態を変更したときは再レンダリングが行われますが、useRefの状態を変更したときは行われません。

const App: React.FC<{}> = () => {
  const [count, setCount] = useState<number>(0);
  console.log(count); // 0 と 1 が出力されます
  useEffect(() => {
    setCount(1);
  }, []);
  return <></>;
};
const App: React.FC<{}> = () => {
  const count = useRef<number>(0);
  console.log(count.current); // 0 だけが出力されます
  useEffect(() => {
    count.current = 1;
  }, []);
  return <></>;
};

useStateで今の状態から次の状態を作るとき、次のように実装すると意図した結果が得られません。

const App: React.FC<{}> = () => {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount(count + 1);
    setCount(count + 1);
  }
  return <>
    {/* 1 クリックにつき count は 1 しか増えません */}
    <button onClick={handleClick}>Click me!</button>
    {count}
  </>;
};

意図した結果を得るためには、setCountに関数を渡します。この関数の第一引数には今の状態が渡ります。関数の戻り値が次の状態になります。

const App: React.FC<{}> = () => {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount(count => count + 1);
    setCount(count => count + 1);
  }
  return <>
    {/* 1 クリックにつき count は 2 増える */}
    <button onClick={handleClick}>Click me!</button>
    {count}
  </>;
};

useEffectに渡した関数が実行されるタイミングは、レンダリングが完了した後です。よって、次のコードは必ずレンダリング後の要素の幅と高さを出力します。

const App: React.FC<{}> = () => {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (ref.current === null) return;
    const style = window.getComputedStyle(ref.current);
    console.log(style.width, style.height); //=> 1440px 18px
  }, []);
  return <div ref={ref}>test</div>;
};

useRefcurrentの変更を検知したいことがあります。しかし、useRefにはcurrentの値の変更を検知する方法は用意されていません。変更を検知したいときは、代わりに「コールバックref」を使います。コールバックrefの具体例としては、canvas要素が存在するときに限りコンテキストを取得したい、というケースがあります。

次のコードは、canvas要素が存在すればsetContextを実行し、存在しなければ何もしないコードです。

const App: React.FC<{}> = () => {
  const [show, setShow] = useState(true);
  const [, setContext] = useState<CanvasRenderingContext2D | null>(null);
  useEffect(() => {
    // 1秒毎にcanvas要素の存在の有無を切り替えます
    const id = window.setInterval(() => setShow(show => !show), 1000);
    return () => window.clearInterval(id);
  }, []);
  // useCallbackに渡した関数は、nodeが変わったときに呼ばれます
  const callbackRef = useCallback((node: HTMLCanvasElement | null) => {
    if (node === null) return;
    setContext(node.getContext('2d'));
  }, []);
  return <>
    {show && <canvas ref={callbackRef}></canvas>}
  </>;
};

同等のことをコールバックrefなしで実現しようとすると、おそらく複雑なコードになります。


useStatesetXXXを実行した後のレンダリングのタイミングは遅延されることがあります。たとえば次のコードは、setCount(1)実行後に即座にレンダリングされず、setCount(2)実行後にレンダリングされます。よってコンソールには02が出力されます。

const Child: React.FC<{ count: number }> = ({ count }) => {
  console.log(count);
  return <></>;
};

const App: React.FC<{}> = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    (async () => {
      // await null;
      setCount(1);
      setCount(2);
    })();
  }, []);
  return <Child count={count} />;
};

しかし、// await null;のコメントアウトを外すだけで、コンソールには012が出力されるようになります。これは、setCount(1)実行後に即座にレンダリングされることを意味します。

このように、setXXX実行後にレンダリングされることは保障されていますが、実行後のどのタイミングでレンダリングされるかについては保障されていません。したがって、タイミングへの依存はなくす必要があります。

1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?