react + typescriptのプロジェクトで、 コンポーネントのpropsの値にジェネリクスで動的に型を定義、 forward refで親コンポーネントから特定の関数を任意のタイミングで実行できるようにする時の記述方法をまとめました。

いくつか方法はあるみたいですが、以下の書き方がすっきりしてて良いのかなと思いました。 以下、ソースコードになります。

型定義の更新

index.d.ts
import React from 'react'

declare module 'react' {
  function forwardRef<T, P>(
    render: (props: P, ref: React.Ref<T>) => React.ReactElement | null,
  ): (props: P & React.RefAttributes<T>) => React.ReactElement | null
}

アロー関数ではなく通常のfunctionによる関数定義で実現する場合は、forwardRefの型定義を更新しなければいけないみたいです。 ので、私の場合はindex.d.tsでReact.forwardRefの定義を更新しました。

コンポーネント

今回のサンプルソースコードです。 Props(SampleListProps)がによってジェネリクスによって定義されています。 SampleListRefが、refを使って実行できる関数の定義になります。 useImperativeHandleを使って実際の挙動を実装していきます。

SampleList.tsx
import React, { forwardRef, useImperativeHandle, useState } from 'react'
import './SampleList.css'

interface SampleListItem<KEY> {
  key: KEY
  value: string
}

interface SampleListProps<KEY> {
  items: SampleListItem<KEY>[]
  onClickItem: (item: SampleListItem<KEY>) => void
}

export interface SampleListRef {
  resetSelectItem: () => void
}

function SampleList<KEY>(
  { items, onClickItem }: SampleListProps<KEY>,
  ref: React.ForwardedRef<SampleListRef>,
) {
  // state
  const [currentItem, setCurrentItem] = useState<SampleListItem<KEY> | null>(null)

  // handle
  useImperativeHandle(ref, () => ({
    resetSelectItem: () => {
      setCurrentItem(null)
    },
  }))

  return (
    <div>
      <ul>
        {items.map((_item) => (
          <li
            key={`${_item.key}`}
            className={currentItem?.key === _item.key ? 'selected' : 'not-selected'}
            onClick={() => {
              setCurrentItem(_item)
              onClickItem(_item)
            }}
          >
            {_item.value}
          </li>
        ))}
      </ul>
    </div>
  )
}

export default forwardRef(SampleList)

setCurrentItem()のuseStateがちゃんと動いているか確認のためのcss定義

SampleList.css
.selected {
    background-color: aquamarine;
}

.not-selected {
    background-color: azure;
}

使い方

Test.tsx
import { createRef, useMemo } from 'react'
import SampleList, { SampleListRef } from './SampleList'

const Test = () => {
  const sampleListRef = createRef<SampleListRef>()

  const items = useMemo(
    () => [
      {
        key: 'key1',
        value: 'value1',
      },
      {
        key: 'key2',
        value: 'value2',
      },
      {
        key: 'key3',
        value: 'value3',
      },
    ],
    [],
  )

  const onClickReset = () => {
    sampleListRef.current?.resetSelectItem()
  }

  return (
    <div>
      <SampleList
        ref={sampleListRef}
        items={items}
        onClickItem={(_item) => {
          console.log('onClickItem()', _item)
        }}
      />

      <button onClick={onClickReset} type='button'>
        リセット
      </button>
    </div>
  )
}

export default Test

createRefでSampleListのrefを取得します。 onClickReset()関数の内部で、refを使ってresetSelectItem()関数を呼び出し、SampleListのリセットを行なっています。

以上がソースコードでした。 他にも定義方法がありそうなので、調べてみようかなと思います。

参考

以下のサイトを参考にさせていただきました、ありがとうございました。

https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/forward_and_create_ref/