1

I am using React context to pass data through a particular journey in my app, and I have come to find that at the last step where I am rendering to the DOM and I want to perform a specific function within my useEffect (it makes data layer push) that a sibling component is causing a re-render and the function is being triggered multiple times.

The sibling component in question triggers a function held within the context to update one of the context's values - Which will cause the context to re-render, which I believe all of my headaches are coming.

Is there a way in which I can get around this such that I do indeed trigger the context value update, but don't cause the component with the data layer push to be re-rendered?

See below for code breakdown - Full codesandbox here

My console logs are showing that things are rerendering twice as I would expect as my Wizard component is updating the context value.

enter image description here

I understand that the ObjectUpdater updating the context forces it to rerender, however I was under the impression that having memo wrap the exported GaPushComponent component would stop it from rerendering if nothing inside of it had changed? Or is it being forced to rerender because the parent is?


Extra research I've been looking into this more, and found this article that explicitly states:

If you are passing down an object on your React context provider and any property on it updates, what happens? Any component which consumes that context will re-render.


Code breakdown:

App.js

export default function App() {
  useEffect(() => console.log("App.js"), []);
  const [wizardOrNothing, setWizardOrNothing] = useState(null);
  const toggleWizard = () =>
    setWizardOrNothing((prev) => {
      return prev === null ? <Wizard /> : null;
    });

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <button onClick={toggleWizard}>Start</button>
      {wizardOrNothing}
    </div>
  );
}

MyContextProvider.js

const MyContextProvider = ({ children }) => {
  const [myVal, setMyVal] = useState(null);
  const [someObj, setSomeObj] = useState({});

  const updateMyVal = (val) => setMyVal(myVal);
  const updateSomeObj = (obj) => setSomeObj((prev) => ({ ...prev, ...obj }));

  useEffect(() => console.log("MyContext.js"), []);

  return (
    <AppContext.Provider
      value={{
        myVal,
        someObj,
        updateMyVal,
        updateSomeObj
      }}
    >
      {children}
    </AppContext.Provider>
  );
};

export default memo(MyContextProvider);

Wizard.js

const Wizard = () => {
  useEffect(() => console.log("Wizard.js"), []);

  return (
    <MyContextProvider>
      <GaPushComponent />
      <ObjectUpdater />
    </MyContextProvider>
  );
};

ObjectUpdater.js

const ObjectUpdater = () => {
  const appContext = useContext(AppContext);

  useEffect(() => {
    console.log("ObjectUpdater.js");
    appContext.updateSomeObj({ newData: "some new data" });
  }, []);

  return <div data-testid="object-updater" />;
};

export default memo(ObjectUpdater);

GaPushComponent.js

const GaPushComponent = () => {
  useEffect(() => {
    console.log("GaPushComponent.js");

    console.warn("pushing data to GA...");
  }, []);

  return <div>My GA push component</div>;
};

export default memo(GaPushComponent);
physicsboy
  • 5,656
  • 17
  • 70
  • 119
  • did you tried passing empty dependency array ```[ ]``` in your useEffect? – Satyam Saurabh Dec 16 '22 at 09:52
  • @SatyamSaurabh apologies - I did actually mean to include an empty dep array on the GaPushComponent.js useEffect - Added it now and I still receive the same rerender. – physicsboy Dec 16 '22 at 10:09

1 Answers1

0

So 2 things

  • My App re-renders twice - this is by design. I see in your demo the app is using React 18. In R18 by design, it runs setup and cleanup one extra time before the actual setup. So the logs you're seeing in there are correct. Add a cleanup function to all your hooks. You can try downgrading to 17 which will prevent this re-render(Strongly advice not too).

    Something to note: this is in dev mode only (When StrictMode is turned on). This doesn't occur in prod builds

  • Your context provider doesn't expose updateSomeObj.

Articles:

  • Could you advise on adding a cleanup function to the hooks? Not 100% sure on what you mean by this. And `updateSomeObj` is exposed, otherwise I wouldn't be able to use it? – physicsboy Dec 16 '22 at 13:01
  • [Codesandbox](https://stackblitz.com/edit/vitejs-vite-dpjre7?file=src%2FApp.jsx,src%2FWizard.jsx,src%2FObjectUpdater.jsx,src%2FMyContext.jsx,src%2Fmain.jsx,src%2FGaPushComponent.jsx&terminal=dev) The `MyContext.js` file shows you an example of cleaning up hooks after usage. Here's another article which explains hook cleanup in depth [Logrocket: Understanding React’s useEffect cleanup function](https://blog.logrocket.com/understanding-react-useeffect-cleanup-function/) – Jason Francis Dec 16 '22 at 13:20
  • @john - I understand what you mean, I just didn't know what you meant in relation to my code. I don't have anything that needs to be cancelled really, so that's ok. Do you have any thoughts on how to get around the rerendering issue? An alternative to context perhaps? – physicsboy Dec 16 '22 at 14:08
  • Updated my [sandbox](https://stackblitz.com/edit/vitejs-vite-dpjre7?file=src%2FApp.jsx,src%2FWizard.jsx,src%2FObjectUpdater.jsx,src%2FMyContext.jsx,src%2Fmain.jsx,src%2FGaPushComponent.jsx&terminal=dev). I have added a ref which updates on first render and doesn't after. Something to note, This happens in dev mode only when `StrictMode` is turned on. On prod your application should only render it once – Jason Francis Dec 16 '22 at 14:26
  • Ah, clever. I will try it out – physicsboy Dec 16 '22 at 15:11
  • Hmm... It didn't seem to affect anything, but I will investigate further. The function that is being triggered is actually a `useEffect` so might be a bit more intricate with how we block it from rerendering. – physicsboy Dec 16 '22 at 15:28