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

Viewed 1884

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

this will not work if you access clickFuntion in children from provider because every time you updating state, provider Object will be recreated and if you wrap this object in useMemolike this:

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

it will be recreated each time clickFunction is fired.

instead you need to pass it as prop to the children like this:

import React, {useContext} from "react";

import Speaker from "./Speaker";

import { GlobalContext } from './GlobalState';

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

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

and for provider object no need to add useMemo to the function clickFunction it's already wrapped in useCallback equivalent to useMemo(()=>fn,[]):

  const provider = {
        clickFunction,
        speakerList,
      }

and for speaker component you don't need global context :

import React from "react";

export default React.memo(({ speaker,clickFunction }) => {
  console.log("render")
  return (
    <>
      <button
        onClick={() => {
          clickFunction(speaker.id);
        }}
      >
        {speaker.name} {speaker.id}{" "}
        {speaker.favorite === true ? "true" : "false"}
      </button>
    </>
  );
});
Related