【问题标题】:Problem with Re-rendering when passing a React function with React Context API使用 React Context API 传递 React 函数时重新渲染的问题
【发布时间】:2020-11-21 19:26:14
【问题描述】:

我有一个简单的示例,我将 clickFunction 作为值传递给 React Context,然后在子组件中访问该值。尽管我使用的是 React.memo 和 React.useCallback,但该子组件会重新呈现事件。我在 stackblitz 中有一个例子,它没有重新渲染问题 without 在此处使用上下文:

https://stackblitz.com/edit/react-y5w2cp(这个没问题)

但是,当我添加上下文并将函数作为上下文值的一部分传递时,所有子组件都会重新渲染。此处显示问题的示例:

https://stackblitz.com/edit/react-wpnmuk

问题代码如下:

Hello.js

import React, { useCallback, useState, createContext } from "react";

import Speaker from "./Speaker";

export const GlobalContext = createContext({});

export default () => {
  const speakersArray = [
    { name: "Crockford", id: 101, favorite: true },
    { name: "Gupta", id: 102, favorite: false },
    { name: "Ailes", id: 103, favorite: true },
  ];

  const [speakers, setSpeakers] = useState(speakersArray);

  const clickFunction = useCallback((speakerIdClicked) => {
    setSpeakers((currentState) =>
      currentState.map((rec) => {
        if (rec.id === speakerIdClicked) {
          return { ...rec, favorite: !rec.favorite };
        }
        return rec;
      })
    );
  }, []);

  return (
    <GlobalContext.Provider
      value={{
        clickFunction: memoizedValue,
      }}
    >
      {speakers.map((rec) => {
        return <Speaker speaker={rec} key={rec.id}></Speaker>;
      })}
    </GlobalContext.Provider>
  );
};

Speaker.js

import React, {useContext} from "react";

import { GlobalContext } from "./Hello";

export default React.memo(({ speaker }) => {
  console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);

  const { clickFunction } = useContext(GlobalContext);

  return (
    <button
      onClick={() => {
        clickFunction(speaker.id);
      }}
    >
      {speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
    </button>
  );
});

以下答案的工作代码

Speaker.js

从“react”导入反应,{ useContext};

import { GlobalContext } from "./Hello";

export default React.memo(({ speaker }) => {
  console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);

  const { clickFunction } = useContext(GlobalContext);

  return (
    <button
      onClick={() => {
        clickFunction(speaker.id);
      }}
    >
      {speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
    </button>
  );
});

Hello.js

import React, { useState, createContext, useMemo } from "react";

import Speaker from "./Speaker";

export const GlobalContext = createContext({});

export default () => {
  const speakersArray = [
    { name: "Crockford", id: 101, favorite: true },
    { name: "Gupta", id: 102, favorite: false },
    { name: "Ailes", id: 103, favorite: true },
  ];

  const [speakers, setSpeakers] = useState(speakersArray);

  const clickFunction = (speakerIdClicked) => {
    setSpeakers((currentState) =>
      currentState.map((rec) => {
        if (rec.id === speakerIdClicked) {
          return { ...rec, favorite: !rec.favorite };
        }
        return rec;
      })
    );
  };
  const provider = useMemo(() => {
    return ({clickFunction: clickFunction});
  }, []);
  return (
    <GlobalContext.Provider value={provider}>
      {speakers.map((rec) => {
        return <Speaker speaker={rec} key={rec.id}></Speaker>;
      })}
    </GlobalContext.Provider>
  );
};

【问题讨论】:

  • React.memo 只处理传递的道具。反应上下文不是传递的道具,因此正常的渲染规则仍然适用。此外,当上下文中的状态更新时,您正在为该值创建一个新对象。

标签: reactjs react-hooks


【解决方案1】:

这是因为您传递给您的提供商的value 每次都会更改。因此,这会导致重新渲染,因为您的 Speaker 组件认为 value 已更改。

也许你可以使用这样的东西:

const memoizedValue = useMemo(() =&gt; ({ clickFunction }), []);

并从函数定义中删除useCallback,因为useMemo 将为您处理这部分。

const clickFunction = speakerIdClicked =>
  setSpeakers(currentState =>
    currentState.map(rec => {
      if (rec.id === speakerIdClicked) {
        return { ...rec, favorite: !rec.favorite };
      }
      return rec;
    })
  );

并将其传递给您的提供商,例如:

<GlobalContext.Provider value={memoizedValue}>
  <Speaker speaker={rec} key={rec.id} />
</GlobalContext.Provider>

在提供答案后,我意识到您使用 Context 不知何故错误。您正在映射一个数组并为每个数据创建多个提供程序。你可能应该改变你的逻辑。

更新:

大多数情况下,您希望将状态保留在您的上下文中。因此,您也可以从value 获得它。在下面提供一个工作示例。这次要小心这个函数,我们使用useCallback来获得一个稳定的引用。

const GlobalContext = React.createContext({});

const speakersArray = [
  { name: "Crockford", id: 101, favorite: true },
  { name: "Gupta", id: 102, favorite: false },
  { name: "Ailes", id: 103, favorite: true },
];

function App() {
  const [speakers, setSpeakers] = React.useState(speakersArray);

  const clickFunction = React.useCallback((speakerIdClicked) => {
    setSpeakers((currentState) =>
      currentState.map((rec) => {
        if (rec.id === speakerIdClicked) {
          return { ...rec, favorite: !rec.favorite };
        }
        return rec;
      })
    );
  }, []);

  const memoizedValue = React.useMemo(() => ({ speakers, clickFunction }), [
    speakers,
    clickFunction,
  ]);

  return (
    <GlobalContext.Provider value={memoizedValue}>
      <Speakers />
    </GlobalContext.Provider>
  );
}

function Speakers() {
  const { speakers, clickFunction } = React.useContext(GlobalContext);

  return speakers.map((speaker) => (
    <Speaker key={speaker.id} speaker={speaker} clickFunction={clickFunction} />
  ));
}

const Speaker = React.memo(({ speaker, clickFunction }) => {
  console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);

  return (
    <button
      onClick={() => {
        clickFunction(speaker.id);
      }}
    >
      {speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
    </button>
  );
});

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root" />

【讨论】:

  • 是的,这是正确的答案!但这里clickFunction 确实;nt 改变所以不需要添加它作为依赖项
  • @devserkan,是的,谢谢。我更新了问题,让上下文只换行一次。尽管如此,当我使用你的 memoizedValue 时,我收到一条错误消息“clickFunction 不是函数
  • 在您更新的问题中,您传递了 memoizedValue 错误。这将是您的整个 value 对象,而不是您的函数。只需再次查看我的答案,看看我是如何将其传递给提供者的。
  • 有人评论说 React.memo 没有必要,因为它只查看属性更改。那是对的吗?如果我从 Speaker.js 中删除 react.memo,所有 3 个组件都会在更新时重新渲染。
  • 评论是对的,React.Memo 寻找道具变化。但是,在您的情况下,它是必需的,因为您在该组件中查找 speaker 道具更改。关于Context 部分的评论是正确的,因为您的Context 值不是这个记忆值的一部分。问题是你的value 在你的Context。
【解决方案2】:

当将 value={{clickFunction}} 作为 prop 传递给 Provider 时,当组件重新渲染并将重新创建此对象时,这将使子更新,因此要防止这种情况发生 您需要使用 useMemo 记住该值。

这里是代码:

import React, { useCallback, useState, createContext,useMemo } from "react";

import Speaker from "./Speaker";

export const GlobalContext = createContext({});

export default () => {
  const speakersArray = [
    { name: "Crockford", id: 101, favorite: true },
    { name: "Gupta", id: 102, favorite: false },
    { name: "Ailes", id: 103, favorite: true },
  ];

  const [speakers, setSpeakers] = useState(speakersArray);

  const clickFunction = useCallback((speakerIdClicked) => {
    setSpeakers((currentState) =>
      currentState.map((rec) => {
        if (rec.id === speakerIdClicked) {
          return { ...rec, favorite: !rec.favorite };
        }
        return rec;
      })
    );
  }, []);
const provider =useMemo(()=>({clickFunction}),[])
  return (
    <div>
      {speakers.map((rec) => {
        return (
          <GlobalContext.Provider value={provider}>
            <Speaker
              speaker={rec}
              key={rec.id}
            ></Speaker>
          </GlobalContext.Provider>
        );
      })}
    </div>
  );
};

请注意,您不再需要使用useCallback clickFunction

【讨论】:

  • 这与我的答案没有什么不同 :) 另外,如果我们不将函数添加到依赖数组中,linter 会发出警告。我知道函数不会改变,但这就是我们向依赖数组添加外部函数的方式。用useCallback 包裹起来,然后添加到依赖数组中。
  • 是的,你是对的,但如果他在 useMemo 中创建函数,他可以删除依赖项,这将是更好的方法 + 不使用 useCallback
  • 哦,是的,没有useCallback,这似乎完全没问题。
  • 旁白:我费了很大的力气才把不工作的例子放到 stackblitz 中。这对回答这样的问题有帮助吗?
  • 返回添加,一次 1 步,我一直在想我明白了……我添加了一个单独的全局状态上下文文件,但仍然重新渲染。 stackoverflow.com/questions/63205047/…
猜你喜欢
  • 2019-03-05
  • 1970-01-01
  • 2018-12-21
  • 2022-01-18
  • 1970-01-01
  • 1970-01-01
  • 2018-09-23
相关资源
最近更新 更多