0

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>
  );
};
1
  • 1
    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. Commented Aug 1, 2020 at 0:19

2 Answers 2

3

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

Sign up to request clarification or add additional context in comments.

6 Comments

This is not a different answer than mine :) Also, if we don't add the function to the dependency array, the linter will fire a warning. I know the function will not change but this is how we add an outsider function to dependency array. Wrap it with useCallback and then add to the dependency array.
yes you are right, but he can remove the dependencies if he create the function inside the useMemo this would be better approach + without using useCallback
Oh yes, without useCallback this seems totally fine.
Aside: I go to a lot of trouble to put the not working example in stackblitz. Is that a help in answering a question like this?
Back add, 1 step at a time, I keep thinking I get it then... I added a separate globalstate context file and still re-render. stackoverflow.com/questions/63205047/…
|
2

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

7 Comments

yes this is the right answer ! but here clickFunction does;nt change so no need to add it as dependencies
@devserkan, yes thanks. I updated the question to have the context just wrap once. Still though when I use your memoizedValue, I get an error saying "clickFunction is not a function
In your updated question you are passing the memoizedValue wrong. It will be your whole value object, not your function. Just look at my answer again to see how I pass it to the provider.
Someone commented that React.memo was not necessary since it only looks at property changes. Is that correct? If I remove react.memo from the Speaker.js, All 3 components re-render on update.
The comment is right, React.Memo looks for prop changes. But, in your case, it is needed because you look for speaker prop change in that component. The comment is right about the Context part because your Context value is not part of this memorized value. The problem was your value in your Context.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.