HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptreactCritical

React with Typescript -- Generics while using React.forwardRef

Submitted by: @import:stackoverflow-api··
0
Viewed 0 times
typescriptwhilewithforwardrefgenericsusingreact

Problem

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 {
  value: OptionValueType;
  label: string;
}

interface WithoutForwardRefProps {
  onChange: (option: OptionType) => void;
  options: OptionType[];
}

export const WithoutForwardRef = (
  props: WithoutForwardRefProps,
) => {
  const { options, onChange } = props;
  return (
    
      {options.map((opt) => {
        return (
           {
              onChange(opt);
            }}
          >
            {opt.label}
          
        );
      })}
    
  );
};


WithForwardRef.tsx

import { Option } from './WithoutForwardRef';

interface WithForwardRefProps {
  onChange: (option: OptionType) => void;
  options: OptionType[];
}

export const WithForwardRef = React.forwardRef(
  (
    props: WithForwardRefProps,
    ref?: React.Ref,
  ) => {
    const { options, onChange } = props;
    return (
      
        {options.map((opt) => {
          return (
             {
                onChange(opt);
              }}
            >
              {opt.label}
            
          );
        })}
      
    );
  },
);


App.tsx

```
import { WithoutForwardRef, Option } from './WithoutForwardRef';
import { WithForwardRef } from './WithForwardRef';

interface CustomOption extends Option {
action: (value: number) => void;
}

const App: React.FC = () => {
return (

Without Forward Ref
Basic
{
// Does type inference on the type of value in the options
console.log('BASIC', option);
}}
/>
Custom

options={[
{
value: 1,
label: 'Test',
action: (value) => {
console.log('ACTION', value);
},
},

Solution

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 = { value: O; label: string; }
type Props> = { 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.

Playground variants 1, 2, 3

Playground variant 4
  1. Use type assertion ("cast")


// Given render function (input) for React.forwardRef
const FRefInputComp = (p: Props, ref: Ref) =>
{p.options.map(o =>
{o.label})}

// Cast the output
const FRefOutputComp1 = React.forwardRef(FRefInputComp) as
(p: Props & { ref?: Ref }) => ReactElement

const Usage11 = () =>
// 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 = (p: P & React.RefAttributes) => ReactElement |null
// `RefAttributes` is built-in type with ref and key props defined
const Comp12 = React.forwardRef(FRefInputComp) as ForwardRefFn
const Usage12 = () => 


  1. Wrap forwarded component



const FRefOutputComp2 = React.forwardRef(FRefInputComp)
// ↳ T is instantiated with base constraint `Option` from FRefInputComp

export const Wrapper = ({myRef, ...rest}: Props & 
  {myRef: React.Ref}) => 

const Usage2 = () => 


  1. 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 = (props: Props & {myRef: Ref}) 
  =>  {props.options.map(o => {o.label})} 
const Usage3 = () => 


  1. 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(
    render: (props: P, ref: ForwardedRef) => ReactElement | null
  ): (props: P & RefAttributes) => 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(render: ForwardRefRenderFunction): 
  ForwardRefExoticComponent & RefAttributes>;


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.

Code Snippets

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 }
]
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} />
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} />
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} />
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
}

Context

Stack Overflow Q#58469229, score: 235

Revisions (0)

No revisions yet.