3

The below question relates to the following sections in the React Context documentation:

  1. Dynamic Context
  2. Updating Context from a Nested Component

Disclaimer: Apologies for all the background information below. It provides context and will hopefully be helpful to future visitors.


What We Know

  • Link 1
    • The (default) context value is set to themes.dark (an object that contains two properties: foreground and background)
    • The default value is only ever used if there are no Providers above the Consumer in the component tree
    • In this case, there is a Provider present in the top-level component (App)
    • This Provider (App), passes down its own state as the context value
    • It is smart to keep the values provided by a Provider equal in structure and type to the default context value (avoids Consumers getting confused)
    • Thus, state in the top-level component (App) holds an object of the same format as the default context value: themes.light
    • Conclusion from the above: When a Consumer reads the context, it reads App's state
    • In other words, we are here using context to pass a parent (App) state deep down in the component tree, without having to pass it through every component in the middle
    • When state in the top-level component (App) changes, it re-renders and a new value for state is provided to the Consumer
    • This way, the Consumer reads the parent's state, via context
    • ...
    • Moving on, we see in link 1 that a function to set state (toggleTheme) is passed down the component tree as a normal prop
    • Thus, in link 1, context only contains an object that reads state
    • We are able to set state in the Consumer by passing the setState function as a normal prop from the Provider's child, down through all the intermediate components, and in to the Consumer
    • Setting the state in the top-level component (App), leads to a re-render of itself, which leads to a re-render of the Provider, which then passes the new App state value down to its Consumer via context
    • As such, the Consumer always knows App's state, via context
    • In conclusion, the flow is:
      1. Parent's state is provided as context value to child Consumer(s)
      2. Parent's state is updated by some child
      3. Parent re-renders
      4. Provider sees that context value (App's state) has changed, and re-renders all its Consumers with the new value
  • Link 2
    • In link 2, we set state in the Consumer, by passing the setState function within the context
    • This differs from link 1, where we relied on a normal prop to set state

Questions

We know from the docs that:

Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes....

All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes.

  1. Let's assume we use a normal variable in App as the context value. We know from the above quote that changing it leads to the Provider re-rendering. Why then, do we bother using state as the context value? What is the benefit of that, vs. just using any normal variable in App?
  2. Both the two approaches above allow us to update state. Why is link 2 incorporating the function to update state within state itself? Could we not just have it as a separate setState function, which is passed to the Consumer via context in an object that has two properties (one is state and the other is the standalone function to update state)?
Magnus
  • 6,791
  • 8
  • 53
  • 84

2 Answers2

3

Let's assume we use a normal variable in App as the context value. We know from the above quote that changing it leads to the Provider re-rendering. Why then, do we bother using state as the context value? What is the benefit of that, vs. just using any normal variable in App?

It's true that when the provider is rerendered with a changed value, any descendents that care about the context will rerender. But you need something to cause the provider to rerender in the first place. This will happen when App's state or its props change (or when you call forceUpdate, but don't do that). Presumably, this is at the top of your application, so there are no props coming in, which means you'll use state to cause it to rerender.

Both the two approaches above allow us to update state. Why is link 2 incorporating the function to update state within state itself? Could we not just have it as a separate setState function, which is passed to the Consumer via context in an object that has two properties (one is state and the other is the standalone function to update state)?

When deciding whether to rerender descendants due to a change of context, react will do basically a === between the old value and the new value. This is super quick and works well with React's preference for immutable data, but when using objects as your value you need to be careful that you're not making new objects on every render. For example, if App is doing something like the following, it will be creating a brand new object every time it renders, and thus will be forcing all the context consumers to rerender as well:

class App extends Component {
  state = {
    data: { 
      hello: 'world',
    }
  }

  updateData() {
    // some function for updating the state  
  }

  render() {
    return (
      <MyContext.Provider value={{ 
        data: this.state.data, 
        updateData: this.updateData
      }} />
    )
  }
}

So the example where they store the function in state is to make sure that the entire value they're providing does not change from one render to another.

Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98
  • Thank you, that is super helpful. On the second question: We update state in order for App to re-render, make the Provider pass a new value down to Consumers, and finally make the Consumers re-render with that new value. Why do we then care that the Consumers re-render based on the full value changing? They have to re-render anyways, right (since the context value updated)? – Magnus Mar 26 '19 at 19:33
  • PS: I was planning to implement the Link 2 example, using hooks. I was therefore planning to combine the state and the function to update state into one object, and use that as the context value. Is that even necessary? Could I just pass the whole useState as the value context? After all, it includes both state and setState. – Magnus Mar 26 '19 at 19:43
  • 1
    App can (conceivably) rerender due to things that have nothing to do the value its providing via context. If this happens, you do **not** want the consumers to rerender. For that to happen, you need the value before and the value after to pass a `===` check. If you're providing an object, that means you can't create a brand new object in App's render method, or you'll end up rerendering consumers unnecessarily. – Nicholas Tower Mar 26 '19 at 20:09
  • Regarding hooks, i'm not sure. It depends whether the array returned by useState is the same array reference from render to render. If it's not, then you'll run into the same problem of unnecessary rerenders. – Nicholas Tower Mar 26 '19 at 20:10
  • Thanks again. I tried to implement the two scenarios just now, and it was great to see that your point on Q1 proved right (i.e. no re-render of Consumers happened). Something interesting happened in Q2: Basically it spiraled into an infinite loop of re-renders. Link: https://codesandbox.io/s/9jln6qy8pp . Is it because of the following: 1) In App, state and function to update state is passed down with Provider in new object. 2) Consumer receives these and proceeds to update parent's state with the updateValue2 callback.... 1/2 – Magnus Mar 26 '19 at 21:22
  • .... 3) Re-render of App causes Provider to re-render Consumers with new state value (all good so far). 4) Consumer calls updateValue2 callback again. 5) Now in App, state should **not** change. So, I am not sure why it goes ahead and re-renders App and Consumers repeatedly, until it breaks. 2/2 – Magnus Mar 26 '19 at 21:25
  • You should never call setState in a render method; it will cause an infinite loop. You have some extra steps, but that's what you're doing. The standard react behavior (ie, no context needed) is when App calls render, all of its children call render too, and then all of their children, etc. Thus ThemedButton will rerender. When it does it calls updateValue, which calls setState on App, which repeats the whole process. ShouldComponentUpdate can improve performance by skipping some of these, but do not rely on it to break infinite loops, as it’s just a hint to react, not a guarantee. – Nicholas Tower Mar 26 '19 at 21:52
  • In your case, even if you had a shouldComponentUpdate which was lucky enough to stop the loop, your use of context would reinstate the loop. When you render this: `value={{ value: this.state.value, updateValue: this.updateValue2 }}`, you are creating a new object every time. Even if this.state.value and this.updateValue2 are unchanged, the object they are wrapped in is new. Thus, the context value has changed and any consumer of the context will get rerendered, tunneling past any component with a shouldComponentUpdate that returns false. – Nicholas Tower Mar 26 '19 at 21:52
  • I think I am getting to the bottom of the confusion here, thanks for your patience. I was under the impression that a re-render **only** happens if state (or props) has changed. In my case, state keeps being set to the same exact value (`value: "cyan"`), thus to my understanding this should **not** cause a re-render. Is it true that if state is set to the same value, a re-render does **not** happen? – Magnus Mar 27 '19 at 08:12
  • Posted a related question here: https://stackoverflow.com/questions/55373878/react-re-rendering-on-setting-state-hooks-vs-this-setstate – Magnus Mar 27 '19 at 09:43
  • No worries on the above, I will post it as a separate SO question (which it probably should be anyways). Thanks a ton for all your insight! – Magnus Mar 27 '19 at 14:41
  • New post can be found here: https://stackoverflow.com/questions/55380624/react-context-when-are-children-re-rendered – Magnus Mar 27 '19 at 15:14
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/190849/discussion-between-magnus-and-nicholas-tower). – Magnus Mar 28 '19 at 14:49
  • @NicholasTower I would like to discuss more about Query1. "All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes"--It clearly says when "Provider's value prop". We extract this value in Consumer. Why do we need to use state variable then, can't we update the value prop inside the Consumer and the parent should re-render? – Dhruv Pandey Oct 22 '20 at 18:15
  • @DhruvPandey To be more precise, "whenever the Provider’s value prop changes" means "whenever the provider rerenders with a new value prop". So first step is the provider needs to rerender. In react, the way you cause a rerender is by setting state. Thus, you need state. The consumer can only cause a rerender of the provider if it can set state in the provider, which means the provider must pass down a function that does so. – Nicholas Tower Oct 23 '20 at 02:55
0

Let's assume we use a normal variable in App as the context value. We know from the above quote that changing it leads to the Provider re-rendering. Why then, do we bother using state as the context value? What is the benefit of that, vs. just using any normal variable in App?

When you use state and update it - it does not matter at all if you are using provider or not - all the provider and components under it will update. Thats under official React Context documentation and is wrong. It means changing provider values DOES NOT call consumer updates at all.

You can validate this by making a separate component with state (which would not be inside provider) and assign that state variable to the provider. So when component state changes, value in state changes and in turn provider should notice this and update consumer. Which it is NOT doing.

In order to update components under consumers, you, unfortunately, have to do it manually. Unless your intension is to update everything under the provider.

This is true as of 2021-04-21 under React 17.0.2 - provider value changes are not being monitored and consumers are not being updated sadly. Unless you put all your provider in component with state, but changing its state forces updating all components under the provider. Sadly.