问题
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