Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Having React Context in Separate File, Can't Get Component to Not re-render

I've got a simple example of React Context that uses useMemo to memoize a function and all child components re-render when any are clicked. I've tried several alternatives (commented out) and none work. Please see code at stackblitz and below.

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

Index.js

import React from "react";
import { render } from "react-dom";
import Hello from "./Hello";

import { GlobalProvider } from "./GlobalState";

function App() {
  return (
    <GlobalProvider>
      <Hello />
    </GlobalProvider>
  );
}

render(<App />, document.getElementById("root"));

GlobalState.js

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

export const GlobalContext = createContext({});

export const GlobalProvider = ({ children }) => {
  const [speakerList, setSpeakerList] = useState([
    { name: "Crockford", id: 101, favorite: true },
    { name: "Gupta", id: 102, favorite: false },
    { name: "Ailes", id: 103, favorite: true },
  ]);

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

  // const provider = useMemo(() => {
  //   return { clickFunction: clickFunction, speakerList: speakerList };
  // }, []);

  //const provider = { clickFunction: clickFunction, speakerList: speakerList };

  const provider = {
    clickFunction: useMemo(() => clickFunction,[]),
    speakerList: speakerList,
  };

  return (
    <GlobalContext.Provider value={provider}>{children}</GlobalContext.Provider>
  );
};

Hello.js

import React, {useContext} from "react";

import Speaker from "./Speaker";

import { GlobalContext } from './GlobalState';

export default () => {
  const { speakerList } = useContext(GlobalContext);

  return (
    <div>
      {speakerList.map((rec) => {
        return <Speaker speaker={rec} key={rec.id}></Speaker>;
      })}
    </div>
  );
};

Speaker.js

import React, { useContext } from "react";

import { GlobalContext } from "./GlobalState";

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>
    </>
  );
});
like image 517
Pete Avatar asked Sep 12 '25 08:09

Pete


1 Answers

Couple of problems in your code:

  • You already have memoized the clickFunction with useCallback, no need to use useMemo hook.

  • You are consuming the Context in Speaker component. That is what's causing the re-render of all the instances of Speaker component.

Solution:

Since you don't want to pass clickFunction as a prop from Hello component to Speaker component and want to access clickFunction directly in Speaker component, you can create a separate Context for clickFunction.

This will work because extracting clickFunction in a separate Context will allow Speaker component to not consume GlobalContext. When any button is clicked, GlobalContext will be updated, leading to the re-render of all the components consuming the GlobalContext. Since, Speaker component is consuming a separate context that is not updated, it will prevent all instances of Speaker component from re-rendering when any button is clicked.

Demo

const GlobalContext = React.createContext({});

const GlobalProvider = ({ children }) => {
  const [speakerList, setSpeakerList] = React.useState([
    { name: "Crockford", id: 101, favorite: true },
    { name: "Gupta", id: 102, favorite: false },
    { name: "Ailes", id: 103, favorite: true }
  ]);

  return (
    <GlobalContext.Provider value={{ speakerList, setSpeakerList }}>
      {children}
    </GlobalContext.Provider>
  );
};

const ClickFuncContext = React.createContext();

const ClickFuncProvider = ({ children }) => {
  const { speakerList, setSpeakerList } = React.useContext(GlobalContext);

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

  return (
    <ClickFuncContext.Provider value={clickFunction}>
      {children}
    </ClickFuncContext.Provider>
  );
};

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

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

function SpeakerList() {
  const { speakerList } = React.useContext(GlobalContext);

  return (
    <div>
      {speakerList.map(rec => {
        return (
          <Speaker speaker={rec} key={rec.id} />
        );
      })}
    </div>
  );
};

function App() {
  return (
    <GlobalProvider>
      <ClickFuncProvider>
        <SpeakerList />
      </ClickFuncProvider>
    </GlobalProvider>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

You can also see this demo on StackBlitz

like image 105
Yousaf Avatar answered Sep 14 '25 21:09

Yousaf