使用 React Context API 传递 React 函数时重新渲染的问题

IT技术 reactjs react-hooks
2021-05-07 07:30:51

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

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

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

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

这是问题代码:

你好.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>
  );
};

演讲者.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>
  );
});

以下答案中的工作代码

演讲者.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>
  );
});

你好.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>
  );
};
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

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

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

const memoizedValue = useMemo(() => ({ 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" />