【问题标题】:React's `memo` drops generics in the returned functionReact 的 `memo` 在返回的函数中删除泛型
【发布时间】:2019-07-04 15:47:52
【问题描述】:

我想将 React 的 memo 用于具有 generic 参数的函数。不幸的是,泛型参数默认为泛型,并且所有花哨的泛型推导逻辑都丢失了(TypeScript v3.5.2)。在下面的示例中,WithMemo(使用 React.memo)失败:

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

WithoutMemo 的工作方式与预期一样。

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

我查看过this question,但我认为这与我的问题无关。

可能的解决方案

我找到了一个使用通用接口的可能解决方案,但它似乎有点粗糙:

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

有没有更优雅的想法来解决这个问题?

【问题讨论】:

  • 好吧,数字没有长度属性,所以编译器是对的。您的代码没有泛型工作,因为它根本不被调用或只用字符串调用,而不是用数字调用。要修复它,您需要仅在传递字符串时添加类型和调用长度检查。
  • @RadosławCybulski - 我不同意。泛型的重点是确保编译器可以将argoutput 类型匹配。添加typeof arg === 'string' 完全消除了泛型类型的优雅。该示例是对我的 typeahead 包中的内容的简化。在那里你可以有大量依赖于泛型决定的依赖,在每个函数中检查输入会太痛苦。
  • 查看您的代码const WithMemo = &lt;MemoTest arg={myArg} output={o =&gt; ${o}:${o.length}} /&gt;;output 中的这段代码需要 o 具有 length 属性,编译器告诉您该数字没有。编译器不会猜到,您总是将字符串传递给这个 Test 实例(否则 lambda 将不起作用)。泛型的整个想法不是通过键入更少的行来使编码更容易,而是通过更早地捕获错误(例如您所做的错误)来使编码更容易。您输入的代码适用于字符串,但不适用于数字,然后您尝试将其推送到数字,编译器会检测到。
  • @RadosławCybulski - 我想这是一个固执己见的问题。我想要弄清楚在调用React.memo 后是否可以拥有通用功能。否则,我可以跳过整个通用逻辑并使用自定义type MyType = string | number,然后按照您的建议到处检查。同样,这个问题不是关于这是否是一个好主意,而是如果可能
  • output: (o: any) =&gt; string; in TProps 定义应该可以工作。您也可以尝试限制 lambda 本身的类型(例如 output={(o: string) =&gt; "${o}: ${o.length}"}output={(o: number) =&gt; "${o * 2}"})。

标签: reactjs typescript


【解决方案1】:

来自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;

【讨论】:

    【解决方案2】:

    作为一种解决方法,我们可以在组件中使用 useMemo。应该够好了。

    【讨论】:

      【解决方案3】:

      要详细说明上述答案,您可以通过浅层比较创建自己的记忆挂钩。它仍然可以避免不必要地渲染您的组件(和任何子组件)。它有点冗长,但这是迄今为止我发现的最好的解决方法。

      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
      }
      

      那么你的代码就变成了

      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)
      

      【讨论】:

      • 作为一个小评论,如果您想编写自己的差异函数(我实际上在某些情况下会这样做),请尝试保持函数快速/快速。您正在创建 uneccesary 对象并使用可枚举的数组(比循环慢得多)。当性能很重要时,这是记忆的重点,我认为有点冗长是可以的;)。
      【解决方案4】:

      一种选择是编写您自己的 HOC,其中包含一个泛型并集成了 React.memo

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

      语法可能有点不对,但你明白了

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-05-04
        • 2020-10-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多