React: 使用 React.memo 和 useContext 钩子防止重新渲染。

创建于 2019-03-19  ·  37评论  ·  资料来源: facebook/react

您要请求功能还是报告错误

漏洞

目前的行为是什么?

我不能通过使用 (useContext hook) 来防止使用 React.memo 进行不必要的重新渲染,从而依赖来自上下文 API 的数据

如果当前行为是错误,请提供重现步骤,如果可能,请提供问题的最小演示。 将链接粘贴到您的 JSFiddle (https://jsfiddle.net/Luktwrdm/) 或 CodeSandbox (https://codesandbox.io/s/new) 示例如下:

React.memo(() => {
const [globalState] = useContext(SomeContext);

render ...

}, (prevProps, nextProps) => {

// How to rely on context in here?
// I need to rerender component only if globalState contains nextProps.value

});

什么是预期行为?

我应该以某种方式访问​​ React.memo 第二个参数回调中的上下文以防止渲染
或者我应该有可能在函数体中返回 react 组件的旧实例。

哪些版本的 React 以及哪些浏览器/操作系统受此问题影响?
16.8.4

最有用的评论

这是按设计工作的。 如果您好奇,可以在https://github.com/facebook/react/issues/14110 中对此进行更长时间的讨论。

假设出于某种原因,您有AppContext的值具有theme属性,并且您只想在appContextValue.theme更改时重新渲染一些ExpensiveTree

TLDR 是现在,您有三个选择:

选项 1(首选):拆分不会一起更改的上下文

如果我们在许多组件中只需要appContextValue.themeappContextValue本身变化太频繁,我们可以将ThemeContextAppContext分开。

function Button() {
  let theme = useContext(ThemeContext);
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
}

现在AppContext任何更改都不会重新渲染ThemeContext消费者。

这是首选的修复方法。 那么你不需要任何特殊的救助。

选项 2:将您的组件一分为二,将memo放在中间

如果由于某种原因你不能拆分上下文,你仍然可以通过将一个组件拆分成两个来优化渲染,并将更具体的 props 传递给内部的一个。 你仍然会渲染外部的,但它应该很便宜,因为它什么都不做。

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"
  return <ThemedButton theme={theme} />
}

const ThemedButton = memo(({ theme }) => {
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
});

选项 3:一个内部包含useMemo组件

最后,我们可以使我们的代码更加冗长,但通过将返回值包装在useMemo并指定其依赖项,将其保留在单个组件中。 我们的组件仍然会重新执行,但如果所有useMemo输入都相同,React 不会重新渲染子树。

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"

  return useMemo(() => {
    // The rest of your rendering logic
    return <ExpensiveTree className={theme} />;
  }, [theme])
}

未来可能会有更多解决方案,但这就是我们现在所拥有的。

不过,请注意,选项 1 更可取——如果某些上下文更改过于频繁,请考虑将其拆分

所有37条评论

这是按设计工作的。 如果您好奇,可以在https://github.com/facebook/react/issues/14110 中对此进行更长时间的讨论。

假设出于某种原因,您有AppContext的值具有theme属性,并且您只想在appContextValue.theme更改时重新渲染一些ExpensiveTree

TLDR 是现在,您有三个选择:

选项 1(首选):拆分不会一起更改的上下文

如果我们在许多组件中只需要appContextValue.themeappContextValue本身变化太频繁,我们可以将ThemeContextAppContext分开。

function Button() {
  let theme = useContext(ThemeContext);
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
}

现在AppContext任何更改都不会重新渲染ThemeContext消费者。

这是首选的修复方法。 那么你不需要任何特殊的救助。

选项 2:将您的组件一分为二,将memo放在中间

如果由于某种原因你不能拆分上下文,你仍然可以通过将一个组件拆分成两个来优化渲染,并将更具体的 props 传递给内部的一个。 你仍然会渲染外部的,但它应该很便宜,因为它什么都不做。

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"
  return <ThemedButton theme={theme} />
}

const ThemedButton = memo(({ theme }) => {
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
});

选项 3:一个内部包含useMemo组件

最后,我们可以使我们的代码更加冗长,但通过将返回值包装在useMemo并指定其依赖项,将其保留在单个组件中。 我们的组件仍然会重新执行,但如果所有useMemo输入都相同,React 不会重新渲染子树。

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"

  return useMemo(() => {
    // The rest of your rendering logic
    return <ExpensiveTree className={theme} />;
  }, [theme])
}

未来可能会有更多解决方案,但这就是我们现在所拥有的。

不过,请注意,选项 1 更可取——如果某些上下文更改过于频繁,请考虑将其拆分

如果主题没有改变,这两个选项都将避免渲染子级。

@gaearon按钮是孩子还是按钮渲染孩子? 我缺少一些如何使用这些的上下文。

使用unstable_Profiler选项 2 仍然会触发onRender回调,但不会调用实际的渲染逻辑。 也许我做错了什么? ~ https://codesandbox.io/s/kxz4o2oyoo~ https://codesandbox.io/s/00yn9yqzjw

我更新了示例以使其更清晰。

使用unstable_Profiler 选项2 仍然会触发onRender 回调,但不会调用实际的渲染逻辑。 也许我做错了什么? https://codesandbox.io/s/kxz4o2oyoo

这正是该选项的重点。 :-)

也许一个很好的解决方案是只有在给定的回调返回 true 时才有可能“获取”上下文和重新渲染组件,例如:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));

我实际遇到的钩子的主要问题是我们无法从钩子内部管理组件返回的内容 - 以防止呈现、返回记忆值等。

如果我们可以,它就不会是可组合的。

https://overreacted.io/why-isnt-xa-hook/#not -a-hook-usebailout

选项 4:不使用上下文进行数据传播,而是使用数据订阅。 使用 useSubscription(因为很难写出涵盖所有情况)。

还有另一种避免重新渲染的方法。
“您需要将 JSX 从重新渲染组件中移出一个级别,然后它就不会每次都重新创建”

更多信息在这里

也许一个很好的解决方案是只有在给定的回调返回 true 时才有可能“获取”上下文和重新渲染组件,例如:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));

我实际遇到的钩子的主要问题是我们无法从钩子内部管理组件返回的内容 - 以防止呈现、返回记忆值等。

而不是这里的真/假......我们可以提供一个基于身份的函数,允许我们从上下文中提取数据吗?

const contextDataINeed = useContext(ContextObj, (state) => state['keyICareAbout'])

除非选择器 fn 的结果与同一函数的先前结果不同,否则 useContext 不会在此组件中弹出。

发现这个库可能是 Facebook 与 hooks 集成的解决方案https://blog.axlight.com/posts/super-performant-global-state-with-react-context-and-hooks/

还有另一种避免重新渲染的方法。
“您需要将 JSX 从重新渲染组件中移出一个级别,然后它就不会每次都重新创建”

更多信息在这里

问题是仅仅为了防止从上到下重新渲染而重构组件树可能代价高昂。

@fuleinist最终,它与 MobX 并没有什么不同,尽管针对特定用例进行了很多简化。 MobX 已经像那样工作(也使用代理),状态发生了变化,使用状态的特定位的组件被重新渲染,没有别的。

@gaearon我不知道我是否遗漏了什么,但我已经尝试了你的第二个和第三个选项,但它们无法正常工作。 不确定这是否只是反应 chrome 扩展程序错误,或者还有其他问题。 这是我的简单表单示例,我看到重新渲染两个输入。 在控制台中,我看到备忘录正在完成他的工作,但 DOM 一直在重新渲染。 我已经尝试了 1000 个项目并且 onChange 事件真的很慢,这就是为什么我认为 memo() 不能正确处理上下文。 感谢您的任何建议:

这是带有 1000 个项目/文本框的演示。 但是在那个演示开发工具中没有显示重新渲染。 您必须在本地下载源代码进行测试: https :

import React, { createContext, useState, useContext, memo } from "react";

const FormContext = createContext();

const FormProvider = ({ initialValues, children }) => {
  const [values, setValues] = useState(initialValues);

  const value = {
    values,
    setValues
  };

  return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
};

const TextField = memo(
  ({ name, value, setValues }) => {
    console.log(name);
    return (
      <input
        type="text"
        value={value}
        onChange={e => {
          e.persist();
          setValues(prev => ({
            ...prev,
            [name]: e.target.value
          }));
        }}
      />
    );
  },
  (prev, next) => prev.value === next.value
);

const Field = ({ name }) => {
  const { values, setValues } = useContext(FormContext);

  const value = values[name];

  return <TextField name={name} value={value} setValues={setValues} />;
};

const App = () => (
  <FormProvider initialValues={{ firstName: "Marr", lastName: "Keri" }}>
    First name: <Field name="firstName" />
    <br />
    Last name: <Field name="lastName" />
  </FormProvider>
);

export default App;

image

另一方面,这种没有上下文的方法可以正常工作,仍在调试中它比我预期的要慢,但至少重新渲染是可以的

import React, { useState, memo } from "react";
import ReactDOM from "react-dom";

const arr = [...Array(1000).keys()];

const TextField = memo(
  ({ index, value, onChange }) => (
    <input
      type="text"
      value={value}
      onChange={e => {
        console.log(index);
        onChange(index, e.target.value);
      }}
    />
  ),
  (prev, next) => prev.value === next.value
);

const App = () => {
  const [state, setState] = useState(arr.map(x => ({ name: x })));

  const onChange = (index, value) =>
    setState(prev => {
      return prev.map((item, i) => {
        if (i === index) return { name: value };

        return item;
      });
    });

  return state.map((item, i) => (
    <div key={i}>
      <TextField index={i} value={item.name} onChange={onChange} />
    </div>
  ));
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

image

@marrkeri我在第一个代码片段中没有发现任何问题。 开发工具中突出显示的组件是使用上下文的Field ,而不是TextField是备忘录组件并实现了 areEqual 函数。

我认为codesandbox示例中的性能问题来自使用上下文的1000个组件。 将其重构为一个使用上下文的组件,例如Fields ,并从该组件(带有地图)为每个值返回一个TextField

@marrkeri我在第一个代码片段中没有发现任何问题。 开发工具中突出显示的组件是使用上下文的Field ,而不是TextField是备忘录组件并实现了 areEqual 函数。

我认为codesandbox示例中的性能问题来自使用上下文的1000个组件。 将其重构为一个使用上下文的组件,例如Fields ,并从该组件(带有地图)为每个值返回一个TextField

正如你所说,我也有同样的想法每次都应该重新渲染,但是 ( ) 仅当值更改时。 但这可能只是开发工具的问题,因为我在周围添加了填充而不是它被重新渲染。 检查这张图片
image

image

我还没有理解你关于重构为一个组件的第二点 . 你能做快照吗? 你们对最大显示数量有什么看法哪些没有滞后? 1000多吗?

@marrkeri我的建议是这样的: https : //codesandbox.io/s/little-night-p985y

这就是为什么 react-redux 在迁移到 Hooks 时不得不停止使用稳定上下文 API 的好处并将当前状态传递给上下文的原因吗?

所以看起来有

选项 4:传递一个 subscribe 函数作为上下文值,就像在遗留上下文时代一样

如果您不控制用法(选项 2-3 需要)并且无法枚举所有可能的选择器(选项 1 需要),但仍想公开 Hooks API,这可能是唯一的选择

const MyContext = createContext()
export const Provider = ({children}) => (
  <MyContext.provider value={{subscribe: listener => ..., getValue: () => ...}}>
    {children}
  </MyContext.provider>
)

export const useSelector = (selector, equalityFunction = (a, b) => a === b) => {
  const {subscribe, getValue} = useContext(MyContext)
  const [value, setValue] = useState(getValue())
  useEffect(() => subscribe(state => {
      const newValue = selector(state)
      if (!equalityFunction(newValue, value) {
        setValue(newValue)
      }
  }), [selector, equalityFunction])
}

@Hypnosphi :由于性能问题和无法摆脱由上下文引起的更新(这使得它不可能基于 v6 方法创建 React-Redux 钩子 API)。

有关更多详细信息,请参阅我的文章React-Redux 的历史和实现

我通读了该线程,但我仍然想知道 - 今天唯一可用的选项是否有条件地重新渲染上下文更改,上面列出了“选项 1 2 3 4”? 是否还有其他工作可以正式解决这个问题,或者“4 种解决方案”是否足够可接受?

我写在另一个线程,但以防万一。 这是一个_非官方_的解决方法。
用户空间中的useContextSelector提议和use-context-selector库。

老实说,这让我觉得这个框架还没有准备好像我们被鼓励的那样完全进入函数和钩子。 使用类和生命周期方法,你可以用一种整洁的方式来控制这些东西,而现在使用钩子,它是一种更糟糕的不太可读的语法

选项 3 似乎不起作用。 我做错了什么吗? https://stackblitz.com/edit/react-w8gr8z

@Martin ,我不同意这一点。

钩子模式可以通过组织良好的文档和代码来阅读
结构和 React 类和生命周期都可以用函数替换
等价的。

不幸的是,反应模式不能通过 React 实现
React 类或钩子。

2020 年 1 月 11 日星期六上午 9:44 Martin Genev通知@github.com
写道:

老实说,这让我觉得这个框架还没有准备好
像我们被鼓励的那样完全进入函数和钩子。 有课
和生命周期方法,你有一个整洁的方式来控制这些事情和
现在使用钩子,这是一种更糟糕的可读性较差的语法


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/facebook/react/issues/15156?email_source=notifications&email_token=AAI4DWUJ7WUXMHAR6F2KVXTQ5D25TA5CNFSM4G7UEEO2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LOIVDNVX6020000043VMVBW63LOIVDNVX520000000000000000000000000000000000000300000003同理心
或取消订阅
https://github.com/notifications/unsubscribe-auth/AAI4DWUCO7ORHV5OSDCE35TQ5D25TANCNFSM4G7UEEOQ
.

@mgenev选项 3 防止重新渲染子项( <ExpensiveTree /> ,名称不言自明)

@Hypnosphi谢谢,那是正确的。
https://stackblitz.com/edit/react-ycfyye

我重写了,现在实际的渲染(显示)组件不会重新渲染,但是所有容器(上下文连接)都会在上下文中的任何道具更改上进行渲染,无论它是否在其中使用。 现在我能看到的唯一选择是开始拆分上下文,但有些东西确实是全局的并且在最高级别包装,并且其中任何道具的更改都会导致整个应用程序中的所有容器都触发,我不不明白这可能对性能有什么好处......

有没有一个很好的例子来说明如何以高性能的方式做到这一点? 官方文档真的很有限

你可以试试我的选项4,这基本上就是react-redux在内部做的
https://github.com/facebook/react/issues/15156#issuecomment -546703046

要实现subscribe功能,您可以使用 Observables 或 EventEmitter 之类的东西,或者自己编写一个基本的订阅逻辑:

function StateProvider({children}) {
  const [state, dispatch] = useReducer(reducer, initialState)
  const listeners = useRef([])
  const subscribe = listener => {
    listeners.current.push(listener)
  }
  useEffect(() => {
    listeners.current.forEach(listener => listener(state)
  }, [state])

  return (
    <DispatchContext.Provider value={dispatch}>
      <SubscribeContext.Provider value={{subscribe, getValue: () => state}}>
          {children}      
      </SubscribeContext.Provider>
    </DispatchContext.Provider>
  );
}

对于那些可能有兴趣的人,这里是与此主题有些相关的各种库的比较。
https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode

我最近的努力是用useTransition检查状态分支的支持,这在即将到来的并发模式中很重要。
基本上,如果我们以正常方式使用 React 状态和上下文,就可以了。 (否则,我们需要一个技巧,例如this 。)

谢谢@dai-shi 我真的很喜欢你的包裹,我正在考虑采用它们。

@dai-shi 嗨,我刚刚找到了你的react-tracked库,如果它像承诺的那样解决了上下文的性能问题,它看起来真的很棒。 使用其他东西仍然是实际的还是更好的? 在这里,我看到了它使用的好例子,它还展示了如何使用use-reducer-async来制作中间件级别https://github.com/dai-shi/react-tracked/blob/master/examples/12_async/src/store .ts谢谢。 目前我已经使用useReducer做了类似的事情,用我自己的异步中间件包装dispatch并使用Context但担心由于上下文包装而导致未来的渲染性能问题。

@bobrosoft react-tracked非常稳定,我想说(肯定是一款开发人员产品)。 非常欢迎反馈,这就是图书馆得到改进的方式。 目前,它在内部使用了 React 的一个未公开的特性,我希望我们将来能够用更好的原语替换它。 use-reducer-async几乎就像一个永远不会出错的简单语法糖。

这个 HoC 对我有用:
````
导入 React, { useMemo, ReactElement, FC } from 'react';
从'lodash/reduce'导入reduce;

type Selector = (context: any) => any;

接口选择器对象{
}

const withContext = (
组件:FC,
上下文:任何,
选择器:SelectorObject,
): FC => {
返回(道具:任何):ReactElement => {
const Consumer = ({ context }: any): ReactElement => {
const contextProps = reduce(
选择器,
(acc: any, selector: Selector, key: string): any => {
const 值 = 选择器(上下文);
acc[key] = 值;
返回acc;
},
{},
);
返回使用备忘录(
(): 反应元素 => ,
[...Object.values(props), ...Object.values(contextProps)],
);
};
返回 (

{(上下文:任何):ReactElement => }

);
};
};

导出默认 withContext;
````

用法示例:

export default withContext(Component, Context, { value: (context): any => context.inputs.foo.value, status: (context): any => context.inputs.foo.status, });

这可以看作是 redux mapStateToProps 的 Context 等价物

我做了一个与 redux 中的 connect() 几乎非常相似的 hoc

const withContext = (
  context = createContext(),
  mapState,
  mapDispatchers
) => WrapperComponent => {
  function EnhancedComponent(props) {
    const targetContext = useContext(context);
    const { ...statePointers } = mapState(targetContext);
    const { ...dispatchPointers } = mapDispatchers(targetContext);
    return useMemo(
      () => (
        <WrapperComponent {...props} {...statePointers} {...dispatchPointers} />
      ),
      [
        ...Object.values(statePointers),
        ...Object.values(props),
        ...Object.values(dispatchPointers)
      ]
    );
  }
  return EnhancedComponent;
};

执行 :

const mapActions = state => {
  return {};
};

const mapState = state => {
  return {
    theme: (state && state.theme) || ""
  };
};
export default connectContext(ThemeContext, mapState, mapActions)(Button);


更新:最终,我切换到 EventEmitter 以使用动态侦听器(鼠标移动时)快速更改粒度数据。 我意识到我是这项工作的更好工具。 Context 非常适合共享数据,但不适用于高刷新率。

它不是归结为声明性订阅或不订阅上下文吗? 有条件地将一个组件包装在另一个使用 useContext() 的组件中。

主要要求是内部组件不能有状态,因为由于分支,它实际上是一个不同的实例。 或者,也许您可​​以预渲染组件,然后使用 cloneElement 更新道具。

一些组件在它的渲染中与上下文无关,但需要从中“读取一个值”。 我错过了什么?
context

在生产中使用Context._currentValue是否安全?

我正在尝试将组件订阅到尽可能便宜地呈现的上下文。 但后来我发现自己像使用 sphagetti 代码或使用备忘录一样以旧方式传递道具,以避免在更新不合逻辑时订阅更新。 备忘录解决方案适用于组件渲染依赖于上下文的情况,否则......

@vamshi9666

我发现https://recoiljs.org/是这个问题的一个很好的解决方案。 我认为如果你将它集成到 React 中会很棒。

@vamshi9666

我只在一个地方使用它,我最初的目标是将状态管理与 jsx 隔离,但也不创建非样板。 所以当我扩展突变功能时,它看起来与 redux 的 reducer 和 action creator 模式非常相似。 哪个更好。 但我认为没有必要重新发明某些东西,因为它实际上已经存在了。

此页面是否有帮助?
0 / 5 - 0 等级