【React】TypeScriptでuseRef を一からまとめてみる

useRefの扱い方がいまいちだったので、まとめてみる。
今回のReactの構造はこんな感じ。
```
src
  ├─ Components
  │    ├─ ChildComp.tsx
  │    └─ ParentComp.tsx 
  └─ App.tsx
```

App.tsxでは単純にParentCompを呼んでいる。
```react
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <ParentComp />
      </header>
    </div>
  );
}
export default App;
```

https://ja.react.dev/reference/react/useRef#usage

# 唯一のプロパティ「current」
useRefは唯一のプロパティ`current`を持ち、
`useRef()`の引数がデフォルトの値となるという。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/4eadf3c8-f152-3831-04e3-029eb7a66f0c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/750ae2da-d50a-2100-dab7-410afff177a0.png)


# 変更がDOMに反映されない

```react
const ParentComp = () => {
  const intervalRef = useRef(0);
  return (
    <div>
      <div>{intervalRef.current}</div>
      <div onClick={()=>intervalRef.current += 1}>add</div>
      <div onClick={()=>console.log(intervalRef)}>check</div>
    </div>
  )
}
```
`add`を押すと加算、`check`を押すと値を確認する処理
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/fe1e7c70-6b70-ebf4-09f7-ba64ea998d2f.png)
`add`を5回押しても見た目は変わらないが、ログを取っていると変数が変わっていることがわかる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/04ba6acc-c71e-6e91-98c2-8f004d91e865.png)

:::note info
ref を変更しても、再レンダーはトリガされない
:::

ref を使用することで、次のことが保証されます。
- レンダーを跨いで情報を保存できます（通常の変数は、レンダーごとにリセットされます）。
- 変更しても再レンダーはトリガされません（state 変数は、変更すると再レンダーがトリガされます）。
- 保存された情報は、コンポーネントのインスタンスごとに固有です（コンポーネントの外側で定義された変数は、コンポーネントのインスタンス間で共有されます）。

## 公式の例
```react
export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}
```
これはクリックするとポップアップが出てくる処理。
クリックがレンダリングに関係ないので、
関数内で変数が完結している。

:::note warn
`ref.current`のままJSXの中で宣言しない。
(レンダー中に ref.current の値を読み取ったり書き込んだりしないでください。)

代わりにイベントハンドラやエフェクトの中で読み書きする必要がある。
:::


# ref で DOM を操作する

https://ja.react.dev/reference/react/useRef#manipulating-the-dom-with-a-ref

TypeScriptだとチュートリアルコード以外にもいろいろ準備しなくてはいけない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/81ec68c9-77d9-8568-eccd-4a659ea9cd60.png)

このようなラジオボタンを作る。
- クリックしたらハイライトされる。
- ボタンを押下で特定のボタンをハイライトする。
- 選択状態をリセットすることができる


これには`useImperativeHandle`というフックを使うと便利に扱える

## useImperativeHandle

https://ja.react.dev/reference/react/useImperativeHandle

`useImperativeHandle` は、ref として公開されるハンドルをカスタマイズするための React フックです。

### メソッドを定義したインターフェースを作成する

```react
export interface ControlledRadioGroupRef {
  select: (id: string) => void;
  deselect: () => void;
}
```
ここでは、外部のコンポーネントで`select`, `deselect` というメソッドを使えるようにするために定義する。

### 子Conponentでrefを受け取れるようにする

```react
const TestInput = forwardRef<ControlledRadioGroupRef, ControlledRadioGroupProps>(
  ({ options, name, selectedValue, onChange }, ref) => {
    const radioRefs = useRef<(HTMLDivElement | null)[]>([]);
```
`forwardRef`を使用して、親コンポーネントから子コンポーネントの DOM 要素や React コンポーネントインスタンスへの参照（ref）を渡す

### useImperativeHandleを定義する
```react
useImperativeHandle(ref, () => ({
  select: (id: string) => {
    const option = options.find((opt) => opt.id === id);
    if (option) {
        onChange(option.value);
    }
  },
  deselect: () => {
    onChange(null);
  },
}));
```
ここは定型文としてこのように書くんだと覚えておこう。
`select`, `deselect`のメソッドの中身を定義している。
ここには詳しい処理の内容を記載する。

### 親Conponentで子Compを操作するメソッドを定義
```react
const handleSelect = (id: string) => {
  radioGroupRef.current?.select(id);
};

const handleDeselect = () => {
  radioGroupRef.current?.deselect();
};
```

`current`があれば各メソッド実行

### DOM例
```react
return (
  <div>
    <ControlledRadioGroup
      ref={radioGroupRef}
      options={options}
      name="exampleRadioGroup"
      selectedValue={selectedValue}
      onChange={handleChange}
    />
    <button onClick={() => handleSelect('option2')}
      className='m-2 border-black border'>
      Select Option 2
    </button>
    <button onClick={handleDeselect}
      className='m-2 border-black border'>
      Deselect All
    </button>
  </div>
);
```


## サンプルコード

```react: TestInput.tsx
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import { CSSProperties } from 'styled-components';

interface RadioOption {
  id: string;
  label: string;
  value: any;
}

interface ControlledRadioGroupProps {
  options: RadioOption[];
  name: string;
  selectedValue: any;
  onChange: (value: any) => void;
}

export interface ControlledRadioGroupRef {
  select: (id: string) => void;
  deselect: () => void;
}

const TestInput = forwardRef<ControlledRadioGroupRef, ControlledRadioGroupProps>(
  ({ options, name, selectedValue, onChange }, ref) => {
    const radioRefs = useRef<(HTMLDivElement | null)[]>([]);

    useImperativeHandle(ref, () => ({
      select: (id: string) => {
        const option = options.find((opt) => opt.id === id);
        if (option) {
          onChange(option.value);
        }
      },
      deselect: () => {
        onChange(null);
      },
    }));

    return (
      <div>
        {options.map((option, index) => (
          <div
            key={option.id}
            ref={(el) => (radioRefs.current[index] = el)}
            onClick={() => onChange(option.value)}
            style={{...styles.button, ...(selectedValue === option.value && styles.selected)}}
          >
            {option.label}
          </div>
        ))}
      </div>
    );
  }
);

export default TestInput;

const styles: { [key: string]: CSSProperties } = {
    inputName: {
      width: "10rem"
    },
  
    button: {
      position: 'relative',
      display: 'inline-block',
      padding: '0 1rem',
      margin: "0 0.25rem",
      borderRadius: "5px",
      backgroundColor: "#FFFFFF",
      boxShadow: "0px 2px 4px rgba(0,0,0,0.5)",
      cursor: "pointer",
      transition: 'all 0.1s ease-in-out',
    },
    selected: {
      backgroundColor: "#b8e6e6",
    },
  };
  
```

```react: TestInputParent.tsx
import React, { useRef, useState } from 'react';
import ControlledRadioGroup, { ControlledRadioGroupRef } from './TestInput';


const TestInputParent: React.FC = () => {
    const [selectedValue, setSelectedValue] = useState<string | null>(null);
    const radioGroupRef = useRef<ControlledRadioGroupRef>(null);
  
    const options = [
      { id: 'option1', label: 'Option 1', value: 'value1' },
      { id: 'option2', label: 'Option 2', value: 'value2' },
      { id: 'option3', label: 'Option 3', value: 'value3' },
    ];
  
    const handleChange = (value: string) => {
      setSelectedValue(value);
      console.log('Selected value:', value);
    };
  
    const handleSelect = (id: string) => {
      radioGroupRef.current?.select(id);
    };
  
    const handleDeselect = () => {
      radioGroupRef.current?.deselect();
    };
  
    return (
      <div>
        <ControlledRadioGroup
          ref={radioGroupRef}
          options={options}
          name="exampleRadioGroup"
          selectedValue={selectedValue}
          onChange={handleChange}
        />
        <button onClick={() => handleSelect('option2')}
          className='m-2 border-black border'>
          Select Option 2
        </button>
        <button onClick={handleDeselect}
          className='m-2 border-black border'>
          Deselect All
        </button>
      </div>
    );
  };
      

export default TestInputParent
```

よくわからないのが型宣言