typescript-cheatsheets / react

Cheatsheets for experienced React developers getting started with TypeScript

Home Page:https://react-typescript-cheatsheet.netlify.app

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Generic Component with forwarded ref.

andrewgreenh opened this issue · comments

Hello everyone,

today I stumbled across the following problem:

I want to build a List Component that has a generic prop . From the outside you can pass a ref to the List, to access a scrollToItem(item: TItem). Since I want to use hooks, I need to use React.forwardRef, which does not allow to return a generic function.

This is my attempt of solving, but I feel like there could be something less verbose and repetitive.

type ListRef<ItemType> = {
  scrollToItem: (item: ItemType) => void;
};

type ListProps<ItemType> = {
  items: ItemType[];
};

const List = forwardRef(function List<ItemType>(props: ListProps<ItemType>) {
  useImperativeHandle<ListRef<ItemType>, ListRef<ItemType>>(ref, () => ({
    scrollToItem: (item: ItemType) => undefined
  }));

  return null;
}) as <ItemType>(
  p: ListProps<ItemType> & { ref: Ref<ListRef<ItemType>> }
) => ReactElement<any> | null;

let ref = useRef<ListRef<number>>(null);

<List items={[1, 2, 3]} ref={ref} />;

TypeScript's inability to retain free type parameters when doing higher order functional programming is unfortunate, this is really the best way to do it if you want to use forwardRef or any other HOC/HOF.

One workaround I use is to just not use forwardRef and just use the olden-days method of innerRef as a prop instead. The React docs say to use useImperativeHandle against formal refs only but that's not a hard constraint:

type ListProps<ItemType> = {
  items: ItemType[]
  innerRef?: React.Ref<{ scrollToItem(item: ItemType): void }>
}

function List<ItemType>(props: ListProps<ItemType>) {
  useImperativeHandle(props.innerRef, () => ({
    scrollToItem() { }
  }))
  return null
}

The React docs say to use useImperativeHandle against formal refs only but that's not a hard constraint:

useImperativeHandle should be used with forwardRef:

-- https://reactjs.org/docs/hooks-reference.html#useimperativehandle

This is more of a recommendation so that you don't leak more implementation detail than necessary. Depending on the use case it might make more sense to use the default ref for a host instance and some actions or handle prop for useImperativeHandle.

commented

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions!