0
votes

I have a simple example where I pass a clickFunction as a value to React Context and then access that value in a child component. That child component re-renders event though I'm using React.memo and React.useCallback. I have an example in stackblitz that does not have the re-render problem without using context here:

https://stackblitz.com/edit/react-y5w2cp (no problem with this)

But, when I add context and pass the the function as part of the value of the context, all children component re-render. Example showing problem here:

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

Here is the problem code:

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

WORKING CODE BELOW FROM ANSWERS BELOW

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

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>
  );
};
2
React.memo only deals with passed props. A react context isn't a passed prop, so normal render rules still apply. Also, when the state in the context updates you are creating a new object for the value.Drew Reese

2 Answers

2
votes

when passing value={{clickFunction}} as prop to Provider like this when the component re render and will recreate this object so which will make child update, so to prevent this you need to memoized the value with useMemo.

here the code:

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

note you dont need to use useCallback anymore clickFunction

1
votes

This is because your value you pass to your provider changes every time. So, this causes a re-render because your Speaker component thinks the value is changed.

Maybe you can use something like this:

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

and remove useCallback from the function definition since useMemo will handle this part for you.

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

and pass this to your provider such as:

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

After providing the answer, I've realized that you are using Context somehow wrong. You are mapping an array and creating multiple providers for each data. You should probably change your logic.

Update:

Most of the time you want to keep the state in your context. So, you can get it from the value as well. Providing a working example below. Be careful about the function this time, we are using useCallback for it to get a stable reference.

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" />