facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
227.85k stars 46.51k forks source link

Preventing rerenders with React.memo and useContext hook. #15156

Closed pumanitro closed 5 years ago

pumanitro commented 5 years ago

Do you want to request a feature or report a bug?

bug

What is the current behavior?

I can't rely on data from context API by using (useContext hook) to prevent unnecessary rerenders with React.memo

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:

React.memo(() => {
const [globalState] = useContext(SomeContext);

render ...

}, (prevProps, nextProps) => {

// How to rely on context in here?
// I need to rerender component only if globalState contains nextProps.value

});

What is the expected behavior?

I should have somehow access to the context in React.memo second argument callback to prevent rendering Or I should have the possibility to return an old instance of the react component in the function body.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React? 16.8.4

gmoniava commented 1 year ago

Maybe I am missing something, but @gaearon maybe we should add clarification to Option 1 that it prevents rerenders in some cases; for example here it doesn't help:

import React from 'react';
import './style.css';

const { useState, createContext, useContext, useEffect, useRef } = React;

const ViewContext = createContext();
const ActionsContext = createContext();

function MyContainer() {
  const [contextState, setContextState] = useState();

  return (
    <ViewContext.Provider value={contextState}>
      <ActionsContext.Provider value={setContextState}>
        <MySetCtxComponent />
        <MyViewCtxComponent />
      </ActionsContext.Provider>
    </ViewContext.Provider>
  );
}

function MySetCtxComponent() {
  const setContextState = useContext(ActionsContext);

  const counter = useRef(0);
  console.log('Set');
  useEffect(() => {
    console.log('=======>>>>>>>>>>>>  Use Effect run in MySetCtxComponent');
    const intervalID = setInterval(() => {
      setContextState('New Value ' + counter.current);
      counter.current++;
    }, 1000);

    return () => clearInterval(intervalID);
  }, [setContextState]);

  return <button onClick={() => (counter.current = 0)}>Reset</button>;
}

function MyViewCtxComponent() {
  const contextState = useContext(ViewContext);
  console.log('View');

  return <div>This is the value of the context: {contextState}</div>;
}

export default MyContainer;

One can see both "View" and "Set" are being logged, which means both components got rerendered.

Wondering the same, have you found any solution for this specific example?

@hrvojegolcic There is one approach, some people do:

function Wrapper(props) {
  const [contextState, setContextState] = useState();
  return (
    <ActionsContext.Provider value={{ contextState, setContextState }}>
      {props.children}
    </ActionsContext.Provider>
  );
}

Now if you change contextState of Wrapper, it will not re-render the children which don't rely on the context. Because react does some optimization (more info): since props.children didn't change (it comes from parent which didn't re-render), it will skip re-rendering it even though Wrapper re-rendered. It will re-render only those children, that rely on context which changed.

hrvojegolcic commented 1 year ago

@gmoniava This is worth to take into consideration, I'll check thanks. But your example from the above is very specific, one component uses only the GET state and another only the SET state, but even with the SET state, it's still the same context and it will indeed behave like both need to re-render. React will understand SET state, as using the context, while it's not really a consumer that needs to re-render.

gmoniava commented 1 year ago

@gmoniava This is worth to take into consideration, I'll check thanks. But your example from the above is very specific, one component uses only the GET state and another only the SET state, but even with the SET state, it's still the same context and it will indeed behave like both need to re-render. React will understand SET state, as using the context, while it's not really a consumer that needs to re-render.

@hrvojegolcic I think why both components re-render in my case is that we updated state at the top parent (contextState), this by default causes all children to re-render, this is standard react behavior and the context providers in between don't prevent any re-renders, that would be a job of memo, if it were in between somewhere.

hrvojegolcic commented 1 year ago

@gmoniava I think I got ya, and it helps. Taking that into consideration then it seems the useMemo/useCallback will do here. As in your example, from my understanding the line <ActionsContext.Provider value={setContextState}> could change to <ActionsContext.Provider value={useCallback(setContextState, [])}>. That way, the setContextState will not report as changed when contextState is changed.

gmoniava commented 1 year ago

@hrvojegolcic No, in my example the components MySetCtxComponent and MyViewCtxComponent re-rendered not because setContextState changed (it didn't). But because I re-rendered the parent MyContainer which caused its children to re-render by default (this is standard react behavior). So in this case, splitting the context doesn't save any re-renders, I suppose gaearon above was talking about different use case. Probably he was having something like this in mind:

let MySetCtxComponent = React.memo(() => {
  const setContextState = useContext(ActionsContext);

  const counter = useRef(0);
  console.log('MySetCtxComponent');
  useEffect(() => {
    const intervalID = setInterval(() => {
      setContextState('New Value ' + counter.current);
      counter.current++;
    }, 1000);

    return () => clearInterval(intervalID);
  }, [setContextState]);

  return <button onClick={() => (counter.current = 0)}>Reset</button>;
});

let MyViewCtxComponent = React.memo(() => {
  const contextState = useContext(ViewContext);
  console.log('MyViewCtxComponent');

  return <div>This is the value of the context: {contextState}</div>;
});

I just wrapped the two components in memo. Now, if you run the original code with these examples, you can see only MyViewCtxComponent re-renders, because the context value which it uses changed.

roggc commented 1 year ago

Take a look at the library react-context-slices. With this library you do it like this:

// slices.js
import getHookAndProviderFromSlices from "react-context-slices";

export const {useSlice, Provider} = getHookAndProviderFromSlices({
  count1: {initialArg: 0},
  count2: {initialArg: 0},
  // rest of slices
});
// app.js
import {useSlice} from "./slices"

const App = () => {
  const [count1, setCount1] = useSlice("count1");
  const [count2, setCount2] = useSlice("count2");

  return <>
    <div>
      <button onClick={()=>setCount1(c => c + 1)}>+</button>{count1}
    </div>
    <div>
      <button onClick={()=>setCount2(c => c + 1)}>+</button>{count2}
    </div>
  </>;
};

export default App;

As you can see it's very easy to use and optimal. The key point is to create as many slices of Context as you need, and this library makes it extremely easy and straightforward to do it.

smitkh commented 1 week ago

If you're experiencing unnecessary re-renders when using the Context API, switching to the useSyncExternalStorehook could solve your issue.

This hook allows you to manage state in a more efficient way, preventing components from re-rendering when they don't need to.

I recently wrote a blog on this topic, explaining how it works and why it's a great alternative to the Context API for global state management.

Check it out here: https://medium.com/@smit-khanpara/enhance-react-performance-replace-context-api-with-usesyncexternalstore-for-better-state-6af420cf7951