patterntypescriptreactCritical
React with Typescript -- Generics while using React.forwardRef
Viewed 0 times
typescriptwhilewithforwardrefgenericsusingreact
Problem
I am trying to create a generic component where a user can pass the a custom
I can get it to work without a forwardRef. Any ideas? Code below:
WithoutForwardRef.tsx
WithForwardRef.tsx
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);
},
},
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
Choose variants (1) or (2) for simplicity. (3) will replace
Playground variants 1, 2, 3
Playground variant 4
This works, as the return type of
Use a custom ref prop instead. This one is my favorite - simplest alternative, a legitimate way in React and doesn't need
Add following code once in your app, perferrably in a separate module
This will augment React module type declarations, overriding
1 Why does the original case not work?
So this function takes a generic component-like render function
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 -
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 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
- 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 = () => - 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 = () => - Omit
forwardRefalltogether
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 = () => - 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.