Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React with Typescript -- Generics while using React.forwardRef

I am trying to create a generic component where a user can pass the a custom OptionType to the component to get type checking all the way through. This component also required a React.forwardRef.

I can get it to work without a forwardRef. Any ideas? Code below:

WithoutForwardRef.tsx

export interface Option<OptionValueType = unknown> {   value: OptionValueType;   label: string; }  interface WithoutForwardRefProps<OptionType> {   onChange: (option: OptionType) => void;   options: OptionType[]; }  export const WithoutForwardRef = <OptionType extends Option>(   props: WithoutForwardRefProps<OptionType>, ) => {   const { options, onChange } = props;   return (     <div>       {options.map((opt) => {         return (           <div             onClick={() => {               onChange(opt);             }}           >             {opt.label}           </div>         );       })}     </div>   ); }; 

WithForwardRef.tsx

import { Option } from './WithoutForwardRef';  interface WithForwardRefProps<OptionType> {   onChange: (option: OptionType) => void;   options: OptionType[]; }  export const WithForwardRef = React.forwardRef(   <OptionType extends Option>(     props: WithForwardRefProps<OptionType>,     ref?: React.Ref<HTMLDivElement>,   ) => {     const { options, onChange } = props;     return (       <div>         {options.map((opt) => {           return (             <div               onClick={() => {                 onChange(opt);               }}             >               {opt.label}             </div>           );         })}       </div>     );   }, ); 

App.tsx

import { WithoutForwardRef, Option } from './WithoutForwardRef'; import { WithForwardRef } from './WithForwardRef';  interface CustomOption extends Option<number> {   action: (value: number) => void; }  const App: React.FC = () => {   return (     <div>       <h3>Without Forward Ref</h3>       <h4>Basic</h4>       <WithoutForwardRef         options={[{ value: 'test', label: 'Test' }, { value: 1, label: 'Test Two' }]}         onChange={(option) => {           // Does type inference on the type of value in the options           console.log('BASIC', option);         }}       />       <h4>Custom</h4>       <WithoutForwardRef<CustomOption>         options={[           {             value: 1,             label: 'Test',             action: (value) => {               console.log('ACTION', value);             },           },         ]}         onChange={(option) => {           // Intellisense works here           option.action(option.value);         }}       />       <h3>With Forward Ref</h3>       <h4>Basic</h4>       <WithForwardRef         options={[{ value: 'test', label: 'Test' }, { value: 1, label: 'Test Two' }]}         onChange={(option) => {           // Does type inference on the type of value in the options           console.log('BASIC', option);         }}       />       <h4>Custom (WitForwardRef is not generic here)</h4>       <WithForwardRef<CustomOption>         options={[           {             value: 1,             label: 'Test',             action: (value) => {               console.log('ACTION', value);             },           },         ]}         onChange={(option) => {           // Intellisense SHOULD works here           option.action(option.value);         }}       />     </div>   ); }; 

In the App.tsx, it says the WithForwardRef component is not generic. Is there a way to achieve this?

Example repo: https://github.com/jgodi/generics-with-forward-ref

Thanks!

like image 331
Primm Avatar asked Oct 20 '19 00:10

Primm


People also ask

Is react forwardRef a hoc?

forwardRef() which means we have to apply the HOC before React.

Why we use forwardRef in react?

The forwardRef method in React allows parent components to move down (or “forward”) refs to their children. ForwardRef gives a child component a reference to a DOM entity created by its parent component in React. This helps the child to read and modify the element from any location where it is used.

How do you use react forwardRef in functional components?

React forwardRef is a method that allows parent components pass down (i.e., “forward”) refs to their children. Using forwardRef in React gives the child component a reference to a DOM element created by its parent component. This then allows the child to read and modify that element anywhere it is being used.


1 Answers

Creating a generic component as output of React.forwardRef is not directly possible 1 (see bottom). There are some alternatives though - let's simplify your example a bit for illustration:

type Option<O = unknown> = { value: O; label: string; } type Props<T extends Option<unknown>> = { options: T[] }  const options = [   { value: 1, label: "la1", flag: true },    { value: 2, label: "la2", flag: false } ] 

Choose variants (1) or (2) for simplicity. (3) will replace forwardRef by usual props. With (4) you globally chance forwardRef type definitions once in the app.

1. Use type assertion ("cast")

// Given render function (input) for React.forwardRef const FRefInputComp = <T extends Option>(p: Props<T>, ref: Ref<HTMLDivElement>) =>   <div ref={ref}> {p.options.map(o => <p>{o.label}</p>)} </div>  // Cast the output const FRefOutputComp1 = React.forwardRef(FRefInputComp) as   <T extends Option>(p: Props<T> & { ref?: Ref<HTMLDivElement> }) => ReactElement  const Usage11 = () => <FRefOutputComp1 options={options} ref={myRef} /> // options has type { value: number; label: string; flag: boolean; }[]  // , so we have made FRefOutputComp generic! 

This works, as the return type of forwardRef in principle is a plain function. We just need a generic function type shape. You might add an extra type to make the assertion simpler:

type ForwardRefFn<R> = <P={}>(p: P & React.RefAttributes<R>) => ReactElement |null // `RefAttributes` is built-in type with ref and key props defined const Comp12 = React.forwardRef(FRefInputComp) as ForwardRefFn<HTMLDivElement> const Usage12 = () => <Comp12 options={options} ref={myRef} /> 

2. Wrap forwarded component

const FRefOutputComp2 = React.forwardRef(FRefInputComp) // ↳ T is instantiated with base constraint `Option<unknown>` from FRefInputComp  export const Wrapper = <T extends Option>({myRef, ...rest}: Props<T> &    {myRef: React.Ref<HTMLDivElement>}) => <FRefOutputComp2 {...rest} ref={myRef} />  const Usage2 = () => <Wrapper options={options} myRef={myRef} /> 

3. Omit forwardRef alltogether

Use a custom ref prop instead. This one is my favorite - simplest alternative, a legitimate way in React and doesn't need forwardRef.

const Comp3 = <T extends Option>(props: Props<T> & {myRef: Ref<HTMLDivElement>})    => <div ref={myRef}> {props.options.map(o => <p>{o.label}</p>)} </div> const Usage3 = () => <Comp3 options={options} myRef={myRef} /> 

4. Use global type augmentation

Add following code once in your app, perferrably in a separate module react-augment.d.ts:

import React from "react"  declare module "react" {   function forwardRef<T, P = {}>(     render: (props: P, ref: ForwardedRef<T>) => ReactElement | null   ): (props: P & RefAttributes<T>) => ReactElement | null } 

This will augment React module type declarations, overriding forwardRef with a new function overload type signature. Tradeoff: component properties like displayName now need a type assertion.


1 Why does the original case not work?

React.forwardRef has following type:

function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>):    ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>; 

So this function takes a generic component-like render function ForwardRefRenderFunction, and returns the final component with type ForwardRefExoticComponent. These two are just function type declarations with additional properties displayName, defaultProps etc.

Now, there is a TypeScript 3.4 feature called higher order function type inference akin to Higher-Rank Types. It basically allows you to propagate free type parameters (generics from the input function) on to the outer, calling function - React.forwardRef here -, so the resulting function component is still generic.

But this feature can only work with plain function types, as Anders Hejlsberg explains in [1], [2]:

We only make higher order function type inferences when the source and target types are both pure function types, i.e. types with a single call signature and no other members.

Above solutions will make React.forwardRef work with generics again.


Playground variants 1, 2, 3

Playground variant 4

like image 169
ford04 Avatar answered Sep 23 '22 21:09

ford04