React's `memo` drops generics in the returned function

允我心安 提交于 2021-02-18 06:04:20

问题


I would like to use React's memo for a function that has a generic argument. Unfortunately the generic argument defaults to the generic and all the fancy generic deduction logic is lost (TypeScript v3.5.2). In the example below WithMemo (using React.memo) fails with:

Property 'length' does not exist on type 'string | number'.
  Property 'length' does not exist on type 'number'.

while the WithoutMemo works just as expected.

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}`} />;

I've looked at this question, but I don't think it relates to my problem.

Possible solution

I've found a possible solution using generic interfaces but it seems a little crude:

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}`} />
  </>
);

Is there a more elegant idea of how to fix this?


回答1:


As a workaround, we can use useMemo inside a component. It should be good enough.




回答2:


To elaborate on the answer above, you can create your own memoisation hook with shallow comparison. It will still avoid unecessary render of your component (and any children). It's a little bit more verbose, but that's the best workaround I've found so far.

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
}

Then your code becomes

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)



回答3:


One option is to write your own HOC that includes a generic and integrates React.memo.

function Memoized<T>(Wrapped) {
    const component: React.FC<T> = props => <Wrapped {...props} />
    return React.memo(component)
}

Syntax might be a little off, but you get the idea




回答4:


From 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;



来源:https://stackoverflow.com/questions/56891234/reacts-memo-drops-generics-in-the-returned-function

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!