reactjs - `memo` de React pierde los genéricos en la función retornada
Me gustaría usar memo de React para una función que tiene un argumento genérico. Desafortunadamente, el argumento genérico por defecto es el genérico y se pierde toda la lógica avanzada de deducción de genéricos (TypeScript v3.5.2). En el ejemplo a continuación, WithMemo (usando React.memo) falla con:
Property 'length' does not exist on type 'string | number'.
Property 'length' does not exist on type 'number'.
mientras que el WithoutMemo funciona tal como se espera.
interface TProps<T extends string | number> {
arg: T;
output: (o: T) => string;
}
const Test = <T extends string | number>(props: TProps<T>) => {
const { arg, output } = props;
return <div>{output(arg)} </div>;
};
const myArg = 'a string';
const WithoutMemo = <Test arg={myArg} output={o => `${o}: ${o.length}`} />;
const MemoTest = React.memo(Test);
const WithMemo = <MemoTest arg={myArg} output={o => `${o}: ${o.length}`} />;
He mirado esta pregunta, pero no creo que se relacione con mi problema.
Posible solución
He encontrado una posible solución usando interfaces genéricas pero parece un poco cruda:
const myArgStr = 'a string';
const myArgNo: number = 2;
const WithoutMemo = (
<>
<Test arg={myArgStr} output={o => `${o}: ${o.length}`} />
<Test arg={myArgNo} output={o => `${o * 2}`} />
</>
);
interface MemoHelperFn {
<T extends string | number>(arg: TProps<T>): JSX.Element;
}
const MemoTest: MemoHelperFn = React.memo(Test);
const WithMemo = (
<>
<MemoTest arg={myArgStr} output={o => `${o}: ${o.length}`} />
<MemoTest arg={myArgNo} output={o => `${o * 2}`} />
</>
);
// Below fail as expected
const FailsWithoutMemo = (
<>
<Test arg={myArgNo} output={o => `${o}: ${o.length}`} />
<Test arg={myArgStr} output={o => `${o * 2}`} />
</>
);
const FailsWithMemo = (
<>
<MemoTest arg={myArgNo} output={o => `${o}: ${o.length}`} />
<MemoTest arg={myArgStr} output={o => `${o * 2}`} />
</>
);
¿Hay una idea más elegante de cómo arreglar esto?
Respuesta #1
Desde https://stackoverflow.com/a/60170425/1747471
interface ISomeComponentWithGenericsProps<T> { value: T; }
function SomeComponentWithGenerics<T>(props: ISomeComponentWithGenericsProps<T>) {
return <span>{props.value}</span>;
}
export default React.memo(SomeComponentWithGenerics) as typeof SomeComponentWithGenerics;
Respuesta #2
Para elaborar sobre la respuesta anterior, puedes crear tu propio hook de memoización con comparación superficial. Aún así evitará el renderizado innecesario de tu componente (y de cualquier hijo). Es un poco más verboso, pero es la mejor solución que he encontrado hasta ahora.
import { ReactElement, useRef } from 'react'
const shallowEqual = <Props extends object>(left: Props, right: Props) => {
if (left === right) {
return true
}
const leftKeys = Object.keys(left)
const rightKeys = Object.keys(right)
if (leftKeys.length !== rightKeys.length) {
return false
}
return leftKeys.every(key => (left as any)[key] === (right as any)[key])
}
export const useMemoRender = <Props extends object>(
props: Props,
render: (props: Props) => ReactElement,
): ReactElement => {
const propsRef = useRef<Props>()
const elementRef = useRef<ReactElement>()
if (!propsRef.current || !shallowEqual(propsRef.current, props)) {
elementRef.current = render(props)
}
propsRef.current = props
return elementRef.current as ReactElement
}
Entonces tu código se vuelve
interface TProps<T extends string | number> {
arg: T
output: (o: T) => string
}
const Test = <T extends string | number>(props: TProps<T>): ReactElement => {
const { arg, output } = props
return <div>{output(arg)}</div>
}
const MemoTest = <T extends string | number>(props: TProps<T>) =>
useMemoRender(props, Test)
