Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
pumanitro opened this issue Mar 19, 2019 · 43 comments
Closed

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

pumanitro opened this issue Mar 19, 2019 · 43 comments

Comments

@pumanitro
Copy link

@pumanitro pumanitro commented Mar 19, 2019

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

@gaearon
Copy link
Member

@gaearon gaearon commented Mar 19, 2019

This is working as designed. There is a longer discussion about this in #14110 if you're curious.

Let's say for some reason you have AppContext whose value has a theme property, and you want to only re-render some ExpensiveTree on appContextValue.theme changes.

TLDR is that for now, you have three options:

Option 1 (Preferred): Split contexts that don't change together

If we just need appContextValue.theme in many components but appContextValue itself changes too often, we could split ThemeContext from AppContext.

function Button() {
  let theme = useContext(ThemeContext);
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
}

Now any change of AppContext won't re-render ThemeContext consumers.

This is the preferred fix. Then you don't need any special bailout.

Option 2: Split your component in two, put memo in between

If for some reason you can't split out contexts, you can still optimize rendering by splitting a component in two, and passing more specific props to the inner one. You'd still render the outer one, but it should be cheap since it doesn't do anything.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"
  return <ThemedButton theme={theme} />
}

const ThemedButton = memo(({ theme }) => {
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
});

Option 3: One component with useMemo inside

Finally, we could make our code a bit more verbose but keep it in a single component by wrapping return value in useMemo and specifying its dependencies. Our component would still re-execute, but React wouldn't re-render the child tree if all useMemo inputs are the same.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"

  return useMemo(() => {
    // The rest of your rendering logic
    return <ExpensiveTree className={theme} />;
  }, [theme])
}

There might be more solutions in the future but this is what we have now.

Still, note that option 1 is preferable — if some context changes too often, consider splitting it out.

@eps1lon
Copy link
Collaborator

@eps1lon eps1lon commented Mar 19, 2019

Both of these options will bail out of rendering children if theme hasn't changed.

@gaearon Are the Buttons the children or do the Buttons render children? I'm missing some context how these are used.

Using the unstable_Profiler option 2 will still trigger onRender callbacks but not call the actual render logic. Maybe I'm doing something wrong? https://codesandbox.io/s/kxz4o2oyoo https://codesandbox.io/s/00yn9yqzjw

@gaearon
Copy link
Member

@gaearon gaearon commented Mar 20, 2019

I updated the example to be clearer.

@gaearon
Copy link
Member

@gaearon gaearon commented Mar 20, 2019

Using the unstable_Profiler option 2 will still trigger onRender callbacks but not call the actual render logic. Maybe I'm doing something wrong? https://codesandbox.io/s/kxz4o2oyoo

That's exactly the point of that option. :-)

@pumanitro
Copy link
Author

@pumanitro pumanitro commented Mar 20, 2019

Maybe a good solution for that would be to have the possibility of "taking" the context and rerender component only if given callback return true e.g:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));

The main problem with hooks that I actually met is that we can't manage from inside of a hook what is returned by a component - to prevent rendering, return memoized value etc.

@gaearon
Copy link
Member

@gaearon gaearon commented Mar 20, 2019

If we could, it wouldn't be composable.

https://overreacted.io/why-isnt-x-a-hook/#not-a-hook-usebailout

@steida
Copy link

@steida steida commented Apr 2, 2019

Option 4: Do not use context for data propagation but data subscription. Use useSubscription (because it's hard to write to cover all cases).

levity added a commit to OasisDEX/mcd-cdp-portal that referenced this issue May 22, 2019
just doing "Option 3" from [this comment](facebook/react#15156), which is just about using useMemo. unfortunately this is not an easy pattern to encapsulate, because you can't call hooks from inside other hooks: https://gist.github.com/levity/3ab3e0f88fd28d55fde5444b9d482f98
@Alfrex92
Copy link

@Alfrex92 Alfrex92 commented Jun 25, 2019

There is another way to avoid re-render.
"You need to move the JSX up a level out of the re-rendering component then it won't get re-created each time"

More info here

@jonnolen
Copy link

@jonnolen jonnolen commented Jul 11, 2019

Maybe a good solution for that would be to have the possibility of "taking" the context and rerender component only if given callback return true e.g:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));

The main problem with hooks that I actually met is that we can't manage from inside of a hook what is returned by a component - to prevent rendering, return memoized value etc.

Instead of a true/false here... could we provide an identity based function that allowed us to subset the data from the context?

const contextDataINeed = useContext(ContextObj, (state) => state['keyICareAbout'])

where useContext wouldn't pop in this component unless the result of the selector fn was different identity wise from the previous result of the same function.

@fuleinist
Copy link

@fuleinist fuleinist commented Jul 12, 2019

found this library that it may be the solution for Facebook to integrate with hooks https://blog.axlight.com/posts/super-performant-global-state-with-react-context-and-hooks/

@fuleinist
Copy link

@fuleinist fuleinist commented Jul 12, 2019

There is another way to avoid re-render.
"You need to move the JSX up a level out of the re-rendering component then it won't get re-created each time"

More info here

Problem is it may be costly to restructure the components tree just to prevent top to bottom re-rendering.

@FredyC
Copy link

@FredyC FredyC commented Jul 13, 2019

@fuleinist Ultimately, it's not that different from MobX, although a lot simplified for a specific use case. MobX already works like that (also using Proxies), the state is mutated and components who use specific bits of the state get re-rendered, nothing else.

@marrkeri
Copy link

@marrkeri marrkeri commented Aug 7, 2019

@gaearon I don't know if I'm missing something, but I have tried yours second and third options and they are not working correctly. Not sure if this is only react chrome extension bug or there is other catch. Here is my simple example of form, where I see rerendering both inputs. In console I see that memo is doing his job but DOM is rerendered all the time. I have tried 1000 items and onChange event is really slow, that's why I think that memo() is not working with context correctly. Thanks for any advice:

Here is demo with 1000 items/textboxes. But in that demo dev tools doesn't show re-render. You have to download sources on local to test it: https://codesandbox.io/embed/zen-firefly-d5bxk

import React, { createContext, useState, useContext, memo } from "react";

const FormContext = createContext();

const FormProvider = ({ initialValues, children }) => {
  const [values, setValues] = useState(initialValues);

  const value = {
    values,
    setValues
  };

  return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
};

const TextField = memo(
  ({ name, value, setValues }) => {
    console.log(name);
    return (
      <input
        type="text"
        value={value}
        onChange={e => {
          e.persist();
          setValues(prev => ({
            ...prev,
            [name]: e.target.value
          }));
        }}
      />
    );
  },
  (prev, next) => prev.value === next.value
);

const Field = ({ name }) => {
  const { values, setValues } = useContext(FormContext);

  const value = values[name];

  return <TextField name={name} value={value} setValues={setValues} />;
};

const App = () => (
  <FormProvider initialValues={{ firstName: "Marr", lastName: "Keri" }}>
    First name: <Field name="firstName" />
    <br />
    Last name: <Field name="lastName" />
  </FormProvider>
);

export default App;

image

On the other hand this approach without context works correctly, still in debug it is slower than I expected but at least rerender is ok

import React, { useState, memo } from "react";
import ReactDOM from "react-dom";

const arr = [...Array(1000).keys()];

const TextField = memo(
  ({ index, value, onChange }) => (
    <input
      type="text"
      value={value}
      onChange={e => {
        console.log(index);
        onChange(index, e.target.value);
      }}
    />
  ),
  (prev, next) => prev.value === next.value
);

const App = () => {
  const [state, setState] = useState(arr.map(x => ({ name: x })));

  const onChange = (index, value) =>
    setState(prev => {
      return prev.map((item, i) => {
        if (i === index) return { name: value };

        return item;
      });
    });

  return state.map((item, i) => (
    <div key={i}>
      <TextField index={i} value={item.name} onChange={onChange} />
    </div>
  ));
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

image

@MarkosKon
Copy link

@MarkosKon MarkosKon commented Aug 7, 2019

@marrkeri I don't see something wrong in the first code snippet. The component that's highlighted in dev tools is the Field that uses the context, not the TextField which is a memo component and implements the areEqual function.

I think the performance problem in the codesandbox example comes from the 1000 components that use the context. Refactor it to one component that uses the context, say Fields, and return from that component (with a map) a TextField for each value.

@marrkeri
Copy link

@marrkeri marrkeri commented Aug 7, 2019

@marrkeri I don't see something wrong in the first code snippet. The component that's highlighted in dev tools is the Field that uses the context, not the TextField which is a memo component and implements the areEqual function.

I think the performance problem in the codesandbox example comes from the 1000 components that use the context. Refactor it to one component that uses the context, say Fields, and return from that component (with a map) a TextField for each value.

As you said I was under same thinking that should be rerendered every time but () only when value is changed. But it is probably just dev tools problems because I've added padding around and instead of it is rerendered. Check this picture
image

image

I haven't catch your second point about refactoring to one component . Could you do snapshot pls? And what do you guys think about maximum displayed number of which are ok without lagging? Is 1000 to much?

@MarkosKon
Copy link

@MarkosKon MarkosKon commented Aug 7, 2019

@marrkeri I was suggesting something like this: https://codesandbox.io/s/little-night-p985y.

godon019 added a commit to dooboolab/dooboo-native-ts that referenced this issue Sep 29, 2019
- no need to declare initial state for using context upfront
- SRP satisfied context(providers)

using multiple providers are preferred
- facebook/react#15156 (comment)
godon019 added a commit to dooboolab/dooboo-native-ts that referenced this issue Sep 30, 2019
* Refactor for using Better Context Api pattern
- no need to declare initial state for using context upfront
- SRP satisfied context(providers)

using multiple providers are preferred
- facebook/react#15156 (comment)

* Fix lint error

* Fix import error

* Refactor ThemeProvider

* Fix Intro test error

* Add using provider guide to Readme

* Add AllProviders and small fix

* Add  tests for ThemeProvider and AppProvider

* Fix import error
@davidecarpini
Copy link

@davidecarpini davidecarpini commented Apr 2, 2020

This HoC worked for me:

import React, { useMemo, ReactElement, FC } from 'react';
import reduce from 'lodash/reduce';

type Selector = (context: any) => any;

interface SelectorObject {
  [key: string]: Selector;
}

const withContext = (
  Component: FC,
  Context: any,
  selectors: SelectorObject,
): FC => {
  return (props: any): ReactElement => {
    const Consumer = ({ context }: any): ReactElement => {
      const contextProps = reduce(
        selectors,
        (acc: any, selector: Selector, key: string): any => {
          const value = selector(context);
          acc[key] = value;
          return acc;
        },
        {},
      );
      return useMemo(
        (): ReactElement => <Component {...props} {...contextProps} />,
        [...Object.values(props), ...Object.values(contextProps)],
      );
    };
    return (
      <Context.Consumer>
        {(context: any): ReactElement => <Consumer context={context} />}
      </Context.Consumer>
    );
  };
};

export default withContext;

usage example:

export default withContext(Component, Context, {
  value: (context): any => context.inputs.foo.value,
  status: (context): any => context.inputs.foo.status,
});

this could be seen as the Context equivalent of redux mapStateToProps

@vamshi9666
Copy link

@vamshi9666 vamshi9666 commented May 1, 2020

I made an hoc almost very similar to connect() in redux


const withContext = (
  context = createContext(),
  mapState,
  mapDispatchers
) => WrapperComponent => {
  function EnhancedComponent(props) {
    const targetContext = useContext(context);
    const { ...statePointers } = mapState(targetContext);
    const { ...dispatchPointers } = mapDispatchers(targetContext);
    return useMemo(
      () => (
        <WrapperComponent {...props} {...statePointers} {...dispatchPointers} />
      ),
      [
        ...Object.values(statePointers),
        ...Object.values(props),
        ...Object.values(dispatchPointers)
      ]
    );
  }
  return EnhancedComponent;
};

Implementation :


const mapActions = state => {
  return {};
};

const mapState = state => {
  return {
    theme: (state && state.theme) || ""
  };
};
export default connectContext(ThemeContext, mapState, mapActions)(Button);


@mikeaustin
Copy link

@mikeaustin mikeaustin commented May 10, 2020

Update: Ultimately, I switched to EventEmitter for fast changing, granular data with dynamic listeners (on mouse move). I realized I was it was the better tool for the job. Context is great for generally sharing data, but not at high refresh rates.

Doesn’t it come down to declaratively subscribing or not subscribing to context? Conditionally wrapping a component in another that uses useContext().

The main requirement is that the inner component can’t have state, since it will effectively be a different instance because of the branching. Or, maybe you can pre-render the component then use cloneElement to update the props.

@yunusbayraktaroglu
Copy link

@yunusbayraktaroglu yunusbayraktaroglu commented Jun 1, 2020

Some components nothing to do with context in it's render but needs to "just read a value" from it. What am I missing?
context

Is Context._currentValue safe to use in production?

I'm trying to subscribe components to contexts which are cheap to render as possible. But then I found myself to passing props like in old way with a sphagetti code or using memo to avoid subscribing for updates when there is nothing logical with updates. Memo solutions are good for when your component render depends on contexts but otherwise ...

@davibe
Copy link

@davibe davibe commented Jul 3, 2020

@vamshi9666 have you use that a lot ? were you able to notice pros/cons of your approach ? I like it a lot. I like the similarity with redux but also how it frees you to write app state management and logic freely as a context

@ImanYZ
Copy link

@ImanYZ ImanYZ commented Aug 2, 2020

I found https://recoiljs.org/ a good solution to this issue. I think it’d be awesome if you integrate it into React.

@vamshi9666
Copy link

@vamshi9666 vamshi9666 commented Nov 6, 2020

@vamshi9666 have you use that a lot ? were you able to notice pros/cons of your approach ? I like it a lot. I like the similarity with redux but also how it frees you to write app state management and logic freely as a context

I used it only in one place and my initial goal is to isolate state management from jsx but also not to create a not of boilerplate. so when i extended mutations functionality , it looked very similar to redux's reducer and action creator pattern. Which is even better. but I don't see a point to reinvent something, when its actually there already.

@fernandoem88
Copy link

@fernandoem88 fernandoem88 commented Jan 19, 2021

try to use a clean context with this library

see this code sandbox example

check also this article

actually, you can create a clean context with createContext from react-hooks-in-callback

Screenshot 2021-01-19 at 15 53 49

and use the useContextSelector hook to pick only the desired part from your context

Screenshot 2021-01-19 at 15 59 47

you can also filter out re-renders noise from derived context's hooks like with this formik example

Screenshot 2021-01-19 at 15 49 38

@arhtudormorar
Copy link

@arhtudormorar arhtudormorar commented Jun 17, 2021

This is working as designed. There is a longer discussion about this in #14110 if you're curious.

Let's say for some reason you have AppContext whose value has a theme property, and you want to only re-render some ExpensiveTree on appContextValue.theme changes.

TLDR is that for now, you have three options:

Option 1 (Preferred): Split contexts that don't change together

If we just need appContextValue.theme in many components but appContextValue itself changes too often, we could split ThemeContext from AppContext.

function Button() {
  let theme = useContext(ThemeContext);
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
}

Now any change of AppContext won't re-render ThemeContext consumers.

This is the preferred fix. Then you don't need any special bailout.

Option 2: Split your component in two, put memo in between

If for some reason you can't split out contexts, you can still optimize rendering by splitting a component in two, and passing more specific props to the inner one. You'd still render the outer one, but it should be cheap since it doesn't do anything.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"
  return <ThemedButton theme={theme} />
}

const ThemedButton = memo(({ theme }) => {
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
});

Option 3: One component with useMemo inside

Finally, we could make our code a bit more verbose but keep it in a single component by wrapping return value in useMemo and specifying its dependencies. Our component would still re-execute, but React wouldn't re-render the child tree if all useMemo inputs are the same.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"

  return useMemo(() => {
    // The rest of your rendering logic
    return <ExpensiveTree className={theme} />;
  }, [theme])
}

There might be more solutions in the future but this is what we have now.

Still, note that option 1 is preferable — if some context changes too often, consider splitting it out.

Hi!
I made a proof of concept on how to benefit from React.Context, but avoid re-rendering children that consume the context object. The solution makes use of React.useRef and CustomEvent. Whenever you change count or lang, only the component consuming the specific proprety gets updated. @gaearon what's your oppinion on this?

Check it out below, or try the CodeSandbox

index.tsx

import * as React from 'react'
import {render} from 'react-dom'
import {CountProvider, useDispatch, useState} from './count-context'

function useConsume(prop: 'lang' | 'count') {
  const contextState = useState()
  const [state, setState] = React.useState(contextState[prop])

  const listener = (e: CustomEvent) => {
    if (e.detail && prop in e.detail) {
      setState(e.detail[prop])
    }
  }

  React.useEffect(() => {
    document.addEventListener('update', listener)
    return () => {
      document.removeEventListener('update', listener)
    }
  }, [state])

  return state
}

function CountDisplay() {
  const count = useConsume('count')
  console.log('CountDisplay()', count)

  return (
    <div>
      {`The current count is ${count}`}
      <br />
    </div>
  )
}

function LangDisplay() {
  const lang = useConsume('lang')

  console.log('LangDisplay()', lang)

  return <div>{`The lang count is ${lang}`}</div>
}

function Counter() {
  const dispatch = useDispatch()
  return (
    <button onClick={() => dispatch({type: 'increment'})}>
      Increment count
    </button>
  )
}

function ChangeLang() {
  const dispatch = useDispatch()
  return <button onClick={() => dispatch({type: 'switch'})}>Switch</button>
}

function App() {
  return (
    <CountProvider>
      <CountDisplay />
      <LangDisplay />
      <Counter />
      <ChangeLang />
    </CountProvider>
  )
}

const rootElement = document.getElementById('root')
render(<App />, rootElement)

count-context.tsx

import * as React from 'react'

type Action = {type: 'increment'} | {type: 'decrement'} | {type: 'switch'}
type Dispatch = (action: Action) => void
type State = {count: number; lang: string}
type CountProviderProps = {children: React.ReactNode}

const CountStateContext = React.createContext<State | undefined>(undefined)

const CountDispatchContext = React.createContext<Dispatch | undefined>(
  undefined,
)

function countReducer(state: State, action: Action) {
  switch (action.type) {
    case 'increment': {
      return {...state, count: state.count + 1}
    }
    case 'switch': {
      return {...state, lang: state.lang === 'en' ? 'ro' : 'en'}
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`)
    }
  }
}

function CountProvider({children}: CountProviderProps) {
  const [state, dispatch] = React.useReducer(countReducer, {
    count: 0,
    lang: 'en',
  })
  const stateRef = React.useRef(state)

  React.useEffect(() => {
    const customEvent = new CustomEvent('update', {
      detail: {count: state.count},
    })
    document.dispatchEvent(customEvent)
  }, [state.count])

  React.useEffect(() => {
    const customEvent = new CustomEvent('update', {
      detail: {lang: state.lang},
    })
    document.dispatchEvent(customEvent)
  }, [state.lang])

  return (
    <CountStateContext.Provider value={stateRef.current}>
      <CountDispatchContext.Provider value={dispatch}>
        {children}
      </CountDispatchContext.Provider>
    </CountStateContext.Provider>
  )
}

function useState() {
  const context = React.useContext(CountStateContext)
  if (context === undefined) {
    throw new Error('useCount must be used within a CountProvider')
  }
  return context
}

function useDispatch() {
  const context = React.useContext(CountDispatchContext)
  if (context === undefined) {
    throw new Error('useDispatch must be used within a AccountProvider')
  }
  return context
}

export {CountProvider, useState, useDispatch}

@qpwo
Copy link

@qpwo qpwo commented Jul 18, 2021

FYI to future readers, there is an open PR for context selectors, so it might happen sometime: reactjs/rfcs#119

@Rajan-Savaliya-spt
Copy link

@Rajan-Savaliya-spt Rajan-Savaliya-spt commented Jul 24, 2021

This is working as designed. There is a longer discussion about this in #14110 if you're curious.

Let's say for some reason you have AppContext whose value has a theme property, and you want to only re-render some ExpensiveTree on appContextValue.theme changes.

TLDR is that for now, you have three options:

Option 1 (Preferred): Split contexts that don't change together

If we just need appContextValue.theme in many components but appContextValue itself changes too often, we could split ThemeContext from AppContext.

function Button() {
  let theme = useContext(ThemeContext);
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
}

Now any change of AppContext won't re-render ThemeContext consumers.

This is the preferred fix. Then you don't need any special bailout.

Option 2: Split your component in two, put memo in between

If for some reason you can't split out contexts, you can still optimize rendering by splitting a component in two, and passing more specific props to the inner one. You'd still render the outer one, but it should be cheap since it doesn't do anything.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"
  return <ThemedButton theme={theme} />
}

const ThemedButton = memo(({ theme }) => {
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
});

Option 3: One component with useMemo inside

Finally, we could make our code a bit more verbose but keep it in a single component by wrapping return value in useMemo and specifying its dependencies. Our component would still re-execute, but React wouldn't re-render the child tree if all useMemo inputs are the same.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"

  return useMemo(() => {
    // The rest of your rendering logic
    return <ExpensiveTree className={theme} />;
  }, [theme])
}

There might be more solutions in the future but this is what we have now.

Still, note that option 1 is preferable — if some context changes too often, consider splitting it out.

This is not working when my context state is global level set, rerender still happed when context change

@Rajan-Savaliya-spt
Copy link

@Rajan-Savaliya-spt Rajan-Savaliya-spt commented Jul 24, 2021

Also In Redux re-render problem not happen, so I change my state management ContextApi to Redux

@Rajan-Savaliya-spt
Copy link

@Rajan-Savaliya-spt Rajan-Savaliya-spt commented Jul 24, 2021

Better Solution: Bigger Application use Redux

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet