Búsqueda de sitios web

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)

Artículos relacionados