2023年中にuseRefについておさらいしておく

こんにちは！！株式会社エイチームライフデザインの @TMDM と申します。
この記事は、[Ateam LifeDesign Advent Calendar 2023 シリーズ1](https://qiita.com/advent-calendar/2023/ateam-life-design)の9日目の記事です。

# useRefについて理解を深める

useRefには主に2つの用途があります。

- DOM要素への参照
  - Reactコンポーネント内の特定のDOM要素に参照できます。参照されたDOMはuseRefで操作が可能です。
- データの保持
  - コンポーネントの再レンダリング間で、データを保持することが可能です。
useStateとは違って、値が変わっても再レンダリングされません。

上記を念頭に置いて、[公式の内容](https://ja.react.dev/reference/react/useRef)を実際に動かして理解していきます。

---

# useRef

> レンダー時には不要な値を参照するための React フックです`
- useRefに値を保持させていても、コンポーネントのレンダリングに直接影響をしませんよ！ということ。


### 使用方法

他のhooksと同じように、トップレベルで呼び出して使用します。

```jsx
import { useRef } from 'react';

const hoge = () =>{
  const inputRef = useRef(null) // 引数に初期値
}
```

### 引数

`useRef(引数)` ← currentプロパティに初期値として設置する値。
2回目以降のレンダーでは無視され、同じオブジェクトを返す。

##### 注意点
- ref.currentはプロパティの書き換えが可能。(stateの一部を持っている場合は変更すべきではない。)
- ref.currentプロパティが変更されても、Reactは再レンダーしません。
- レンダー中にref.currentの値を読んだり書き込んだりはしてはいけない。コンポーネントの振る舞いが予測不可能になる(useState使おう)

## refを使用して値を参照する

```jsx
import { useRef } from 'react';

const hoge = () =>{
  const intervalRef = useRef(0);
  console.log(intervalRef)
```

useRefはcurrentしかプロパティを持ちません！ : [currentの日本語訳はこちら](https://ejje.weblio.jp/content/current)
このcurrentに指定された初期値が、設定された状態のrefを返します。

![SS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/7dea24aa-1f16-d8d0-3e9e-20ccd750e268.png)

#### 再レンダーはトリガされない

ref を変更しても、再レンダーはトリガされません。

本当かな？下記コードで確認してみます。

```jsx
import { FC, useEffect, useRef } from 'react'

export const Test: FC = () => {
  const intervalRef = useRef(0)
  const handleClick = () => {
    intervalRef.current = intervalRef.current + 1
    console.log(intervalRef.current)
  }
  useEffect(() => {
    console.log('hello')
  })

  return <button onClick={handleClick}>クリック</button>
}

```

![SS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/6df8a73c-64f3-4ea3-007b-e71110ffda58.png)

helloは1度きりしか出ませんでした。再レンダリングされていませんね。

#### レンダーを跨いで情報を保存できる
ref は、出力されるコンポーネントの外見に影響しないデータを保存するのに適している。

```jsx
// useStateと組み合わせてみる。
// useRefの値が増えてもレンダーされない。
import { FC, useEffect, useState, useRef } from 'react'

export const Kv: FC = () => {
  const intervalRef = useRef(0)
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount((c) => c + 1)
    console.log(count)
    console.log(intervalRef.current)
  }
  setInterval(() => {
    intervalRef.current = intervalRef.current + 1
  }, 500) // 内部でuseRefのカウントだけどんどん進む

  useEffect(() => {
    console.log('hello')
  })

  return <button onClick={handleClick}>クリック</button>
}

```

![SS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/5807108e-21dc-60e2-e4af-0b9b609b0183.png)

currentの値は0.5秒ごとにどんどん増えていきますが、helloは出まくりません！！


# ref で DOM を操作する

フォーカスボタンを押下すると、inputタグにfocusされる。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/94ed53ae-f1d2-5f79-e7dd-47ea1ea61e22.png" width=300>

```tsx
import React ,{useRef}from 'react'

const Sample = () => {
  const inputRef = useRef(null)
  const handleFocus =()=>{
    inputRef.current.focus() // inputRefにフォーカスして
  }
  return (
    <div>
      <label>
        <input type="text" ref={inputRef} />
        <button onClick={handleFocus}>フォーカスボタン</button>
      </label>
    </div>
  )
}
export default Sample
```

#### refを渡したいのがcomponentだった場合、forwardRefを使用して渡す

子componentの要素をrefを通して操りたい時もありますよね。

親コンポーネントは、直接子のinputにアクセスできませんし、refは通常のpropsとは別に扱われるのでpropsもできません。
そのため、forwardRefを使用して子コンポーネントにrefを渡す必要があります。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/43f791c1-c5f0-807a-066f-fb911b3b573f.png" width=150>

```jsx
import React ,{useRef,forwardRef}from 'react'

const MyInput = forwardRef((props, ref) => { // 必ずこの順序(props, ref)で渡します
  return <div>forwardRef<br/><input {...props} ref={ref} /></div>
});

const Sample = () => {
  const inputRef = useRef(null)
  const handleFocus =()=>{
    inputRef.current.focus()
  }
  return (
    <div>
      <label>
        <MyInput ref={inputRef} />
        <button onClick={handleFocus}>フォーカスボタン</button>
      </label>
    </div>
  )
}

export default Sample
```

forwardRefは玄関みたいですね。
forwardRefは親コンポーネントに対して、内部にアクセスする「入り口」を提供するイメージです。

## ref の値の再生成を防ぐ 

useRefは初回に渡された ref の値を保存しますが、レンダーのたびに呼び出しは発生してしまう。

```jsx
const playerRef = useRef(new 重い処理()); // レンダーのたびに無駄が多い...
```

そのため初期化するのがおすすめ

```jsx
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new 重い処理(); // 初回レンダーだけ、重い処理をnewする。
  }
```

---

[公式](https://ja.react.dev/reference/react/useRef#examples-dom)以外にも、useRefを使ったテクニックが紹介されていました。
[React×IntersectionObserverで実現する3つの強力なフィーチャー！](https://qiita.com/itinerant_programmer/items/632acf6d14a54766693f)
 - 広告の表示率計測 : コンテンツ作ったけど、ユーザーにどれくらい刺ささっているのか？を計測したいときに良さそうですね。


## 終わりに
お疲れ様でした！
useRefは命令的な操作ができます。これはReactの宣言的なパラダイムとは少々異なりますね。
とても便利なのですが、利用の際は慎重になる必要があるかと思います。
「本当にこれでいいのかな？」「よりよい方法はないかな？」
というのは常に考えておきたいですね！