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!
forwardRef() which means we have to apply the HOC before 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.
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.
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.
// 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} />
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} />
forwardRef
alltogetherUse 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} />
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With