Skip to content
This repository has been archived by the owner on Jun 17, 2024. It is now read-only.

How to use useMemo to optimize context consumers #673

Closed
terrencewwong opened this issue Apr 24, 2019 · 12 comments
Closed

How to use useMemo to optimize context consumers #673

terrencewwong opened this issue Apr 24, 2019 · 12 comments

Comments

@terrencewwong
Copy link

terrencewwong commented Apr 24, 2019

Hello!

I read your blog article recently on Application State Management with React

There was something in the article I didn't understand. What is the call to React.useMemo doing in the CountProvider?

function CountProvider(props) {
  const [count, setCount] = React.useState(0)
  const value = React.useMemo(() => [count, setCount], [count])
  return <CountContext.Provider value={value} {...props} />
}

I assumed this would be for preventing rerenders of context consumers, i.e. components that use the custom useCount hook. But I checked out the codesandbox for the article and noticed that both CountDisplay and Counter always rerender when the count changes. Is useMemo required for a different case that's not yet present in the example?

NOTE
Here's the original PR that started this discussion (cc @giossa94): kentcdodds/kentcdodds.com#142

@felipeleusin
Copy link

The dependency of useMemo here means that it will always return a new version when count changes and trigger a rerender.

Both CountDisplay and Counter depend on the value of count so they should be rerendered.

The useMemo here means that if for some reason the CountProvider gets rerendered but the count doesn't change, it doesn't (might) trigger a rerender of the children (because the provider keeps the same value memoized by the render).

Another thing to keep in mind is that useMemo doesn't guarantee that it won't recalculate its values.

@giossa94
Copy link

Yes, that makes sense @felipeleusin.
@terrencewwong I think we were missing something on the original discussion: even if the reference of setCount remains the same across renders of CountProvider, the reference of the array [count, setCount] will change on every render, and hence consumers will re-render. So the purpose of that useMemo is to memoize the array and avoid re-renders if count doesn't change. If we were passing only setCount as the provided value, there would be no need to memoize it.

@terrencewwong
Copy link
Author

terrencewwong commented Apr 26, 2019

@felipeleusin @giossa94 thanks for your responses, I made an example in codesandbox and I see it now! I think the concept is actually quite tricky so I think I might write more about this later. Here's the codesandbox I created: https://codesandbox.io/s/xj5p774ywz

If we were passing only setCount as the provided value, there would be no need to memoize it.

I thought this would be true! But I made a specific hook called just, useSetCount() expecting that I would not need to call useMemo and in the end it didn't turn out to be true. I'm not actually sure why that is.

EDIT:

Ah I think I understand, useSetCount technically depends on count which is why the rerender happens. We would have to do as you said and have a provider that only providers setCount as the value

@felipeleusin
Copy link

felipeleusin commented Apr 26, 2019

I'm not sure and it's early here but if you want to solve this with a hook you need to create a second context that only has the setCount as its value. Since this doesn't change, the components using it don't need to rerender.

That being said, this is most likely a premature optmization.

You would end up with some code like:

const CountContext = React.createContext();
const SetCountContext = React.createContext();

const Provider = () => {
    const [count, setCount] = React.useState(0);
	const value = React.useMemo(() => [count, setCount], [count]);

   return (<SetCountContext.Provider value={setCount}>
		<CountContext.Provider value={value}>{children}</CountContext.Provider>
	</SetCountContext.Provider>)
}

and having an useSetCount like:

const useSetCount = () => {
  return React.useContext(SetCountContext)
}

@patrykkarny
Copy link

Hi guys! I was about to ask the same question, correct me if I'm wrong but I don't see the reason for using useMemo in this example, any change to the state will always return the new value and as described in the docs setCount is a guarantee to be the same across re-renders (https://reactjs.org/docs/hooks-reference.html#usestate). In case of performance issues, I think it is better to use useMemo on the component level while using the context as described here facebook/react#15156 (comment)

@terrencewwong
Copy link
Author

@patrykkarny , I'm working on a blog post about this topic actually. I'll share it with you as soon as I'm done! It's a bit tricky to see why useMemo is useful. In the meantime you can check out the codesandbox that I created https://codesandbox.io/s/xj5p774ywz.

Without useMemo, the component MemoizedIncrement would be rerendered when you press the toggle button.

@kentcdodds
Copy link
Owner

Glad to hear you're making a blog post about it @terrencewwong! I was going to do that today, but now I can write about something else 👍 😄

@patrykkarny
Copy link

hey @terrencewwong, thank you for the codesandbox examples, the topic is quite tricky and interesting, I can't wait to read your blog post about that :D in the meantime I played a little with the examples and it is true that without useMemo in the provider, useCount will return the new reference every time you click the toggle button. I think as soon as you don't have any performance issues it is fine and it is how the normal react flow works. But if you decide to block unnecessary renders you can do it at the component level:

  1. uncomment the line with const value = [count, setCount] and comment out the one with the useMemo inside the Provider
  2. replace the MemoizedIncrement component with this one:
const MemoizedIncrement = () => {
  const [_, setCount] = useCount()

  return React.useMemo(() => {
    console.log('    MemoizedIncrement')

    return (
      <div>
        <button onClick={() => setCount(c => c + 1)}>+1 (memoized)</button>
      </div>
    )
  }, [setCount])
}

@sami616
Copy link

sami616 commented Apr 30, 2019

Look forward to a blog post explaining why useMemo is required here.

I use a little helper function to help create a store and contexts documented here and would be interested to find out if useMemo could help optimize. ✌️

@JustAboutJeff
Copy link

JustAboutJeff commented May 1, 2019

Thanks to @kentcdodds for directing me here! The sandbox from @terrencewwong was useful in validating what I was curious about. Specifically, I wanted to know if we could revise the example from Kent's blog post to remove the call to useMemo and instead rely on object identity from the return value of useState to avoid re-renders. Like so:

original

function CountProvider(props) {
  const [count, setCount] = React.useState(0)
  const value = React.useMemo(() => [count, setCount], [count])
  return <CountContext.Provider value={value} {...props} />
}

revised

function CountProvider(props) {
  const state = React.useState(0)
  return <CountContext.Provider value={state} {...props} />
}

Unfortunately, based on what I'm seeing from the sandbox this will not work. Even if state does not change across renders for CountProvider the wrapping array returned from useState here will not maintain object identity, even though its members (count and setCount) do! This key bit of nuance isn't described in the React docs as they use destructuring in every example!

@alejandronanez
Copy link

Hey y'all!

I've been reading your examples and comments (thanks for sharing what you're learned) but I'm still a bit confused about this.

In this Codesandbox, my App component has a toggle state, I also have a component that doesn't rely on context and a couple of other components that does.
The thing is that whenever I update App's state, everything gets rerendered.

// App.js
import { CountProvider, useCount } from "./count-context";

function DisplayCount() {
  console.log('Rendering DisplayCount');
  const { count } = useCount();

  return <div>DisplayCount: {count}</div>;
}

function NoContextComponent() {
  console.log('Rendering: NoContextComponent');
  return <div>I do not have a context</div>;
}

function IncreaseCount() {
  const { increment } = useCount();

  return (
    <div>
      <button onClick={() => increment()}>Incremenet Count</button>
    </div>
  );
}

function App() {
  const [toggle, setToggle] = React.useState(false);

  return (
    <div className="App">
      <h1>Context + UseMemo</h1>
      <NoContextComponent />
      <IncreaseCount />
      <DisplayCount />
      <button onClick={() => setToggle(!toggle)}>
        Toggle {toggle ? 'Off' : 'On'}
      </button>
    </div>
  );
}
// count-context.js
const CountContext = React.createContext();

const CountProvider = props => {
  const [count, setCount] = React.useState(0);
  const value = React.useMemo(() => {
    return { count, setCount };
  }, [count]);

  return <CountContext.Provider value={value} {...props} />;
};

function useCount() {
  const context = React.useContext(CountContext);

  if (!context) {
    throw new Error("useCount must be used within a CountProvider");
  }

  const { count, setCount } = context;
  const increment = () => setCount(c => c + 1);

  return {
    count,
    increment
  };
}

export { CountProvider, useCount };

React Profiler Results

When the Toggle button is clicked:

React's profiler - Toggle button clicked.
This happens when I click the `Toggle` button, everything gets rendered, except the `CountProvider` which I think it's expected

First click - Toggle button

Second click - Toggle button

When the Increment button is clicked:

React's profiler - Increment button clicked.
This happens when I click the `Increment Count` button, this is what I would expect since `DisplayCount` is a children of `App`, so `App` should not be affected (rerendered)

First click - Increment Count

Second click - Increment Count

@giossa94
Copy link

giossa94 commented May 1, 2019

Hi @alejandronanez !

The thing is that whenever I update App's state, everything gets rerendered.

I think this is expected behavior, since you're calling useState from your Appcomponent, and React will re-render it on every toggle update. To solve this you can wrap you're button into a Button component which holds the toggle state, and that makes sense because none of the other children of App care about the toggle value. This follows @kentcdodds' recommendation of keeping state as close to where it's needed as possible.

I hope this helps.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants