Reactについての知見まとめ

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

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

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

<hr>

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

```tsx
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`に関数を渡します。この関数の第一引数には今の状態が渡ります。関数の戻り値が次の状態になります。

```tsx
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}
  </>;
};
```

<hr>

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

```tsx
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>;
};
```

<hr>

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

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

```tsx
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なしで実現しようとすると、おそらく複雑なコードになります。

<hr>

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

```tsx
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;`のコメントアウトを外すだけで、コンソールには`0`、`1`、`2`が出力されるようになります。これは、`setCount(1)`実行後に即座にレンダリングされることを意味します。

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