Skip to content

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

Closed
Listed in
@pumanitro

Description

@pumanitro

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

Activity

gaearon

gaearon commented on Mar 19, 2019

@gaearon
Collaborator

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

eps1lon commented on Mar 19, 2019

@eps1lon
Collaborator

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

gaearon commented on Mar 20, 2019

@gaearon
Collaborator

I updated the example to be clearer.

gaearon

gaearon commented on Mar 20, 2019

@gaearon
Collaborator

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

pumanitro commented on Mar 20, 2019

@pumanitro
Author

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

gaearon commented on Mar 20, 2019

@gaearon
Collaborator
steida

steida commented on Apr 2, 2019

@steida

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

Alfrex92

Alfrex92 commented on Jun 25, 2019

@Alfrex92

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

jonnolen commented on Jul 11, 2019

@jonnolen

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.

113 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @steida@davibe@mikeaustin@dai-shi@jonnolen

        Issue actions

          Preventing rerenders with React.memo and useContext hook. · Issue #15156 · facebook/react