Do you want to request a feature or report a bug?
Bug
What is the current behavior?
Nested context consumers do not seem to update leaving higher up updates stale:
https://codesandbox.io/s/1qwq93n01q
What is the expected behavior?
The critical piece of code that composes multiple consumers is this one:
let values = []
return [...contextRefs, Wrapped].reduceRight((accumulator, Context, i) => (
<Context.Consumer>
{value => {
values[i] = value
if (accumulator === Wrapped) {
let context = mapContextToProps(...values, props)
context = typeof context === 'object' ? context : { context }
return <Wrapped {...props} {...context} />
} else return accumulator
}}
</Context.Consumer>
))
From a dynamic array of context providers it creates a nested blob of consumers with the receiver sitting at the end (Foo
in this example).
The first consumer fires and does get the value, it then returns the sub-tree (accumulator
, which contains the second consumer, which contains Foo
) but it doesn't actually render, it looks like something in Context.Consumer prevents it.
In the React alphas i believe they would render regardless. I wonder how it would be possible to forward changed values to the actual receiver, i can't call setState in there.
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
React 16.3.1
Can you provide a reproduction that uses Context elements directly and illustrates the bug? It's not clear that the problem is in React from your codesandbox
@jquense
I tried to replicate it as faithfully as i could: https://codesandbox.io/s/7212l1qjlj
As you see, the state is updated, it re-renders the first provider on the state-value change. The consumer receives the value, but the children that it returns aren't updated, the value never reaches the receiving component.
This is pretty hard to read because of all the abstraction. Can you unroll it so there are no HOCs and reduces?
Not saying this is the problem, but it looks shady to me:
Render method (including context consumer's render prop) is supposed to be pure. Using it with a closure like this can lead to weird bugs.
That's the thing, without hoc it works because everything's in the same tree. Something prevents the second consumer from updating when it's separate. It's this condition which provokes the bug. This affects all libraries that allow listening to multiple providers, as they're all using constructs like above.
Render method (including context consumer's render prop) is supposed to be pure. Using it with a closure like this can lead to weird bugs.
It has to transport the values to the wrapped component somehow, which needs to receive all values, not just the ones that change. Perhaps there is a better way, but it's not what's causing the issue.
PS. here's the same with [email protected] --- and it works:
https://codesandbox.io/s/04ky0zlkr0
Something must have changed.
Don't look at 16.4.0 alpha please, it's broken in a myriad of ways and isn't very useful. :-)
In that aspect it works as expected though. The problem is in the current React. If context.Consumer doesn't update (like any React.Component would) it breaks dynamic context subscribers.
I'm sorry鈥擨'd love to help out but I'll need a more reduced example that doesn't mutate anything in closures and that doesn't have HOCs. I don't agree that "without hoc it works because everything's in the same tree": HOC is nothing but an abstraction that generates a component. "Unrolling" HOC manually in the code could produce more code but would not change the semantics of it. React doesn't care if you use HOCs or not.
My guess is that the accumulator is becoming stale. This works:
<context1.Consumer>
{value1 => (
<context2.Consumer>
{value2 => `${value1} ${value2}`}
</context2.Consumer>
)}
</context1.Consumer>
But the moment i do it dynamically (and that's critical), then suddenly it doesn't work:
const values = []
return [context1, context2, View].reduceRight((accumulator, Context, i) => (
<Context.Consumer>
{value => {
values[i] = value
return accumulator === View ? <View values={values} /> : accumulator
}}
</Context.Consumer>
))
The first consumer triggers and return the accumulated result, which is the second consumer, which contains the view. But it becomes stale. This works in react-broadcast, create-react-context and all previous React alphas.
If you know how else to create a dynamic listener that can listen to multiple context providers, maybe that would help it.
Again, I don't think the logic with pushing something into a closure will work. It's not obvious to me why it doesn't (I'd need to spend time looking into it) but it's just not supported to have impure render like this.
I understand. I only wonder how one would solve this ...
If given a list of providers, say
const providers = [context1, context2, context3]
and a view, say
const View = ({ a, b, c }) => `${a} ${b} ${c}`
how would you create a Context.Consumer construct dynamically that has the view as the deepest element, receiving all values? So that this is the outcome:
<context1.Consumer>
{value1 =>
<context1.Consumer>
{value2 =>
<context1.Consumer>
{value3 => <View a={value1} b={value2} c={value3} />}
</context1.Consumer>
}
</context1.Consumer>
}
</context1.Consumer>
And doing it pure ... is that even possible at all, given that consumer values are only obtained by callback?
Do you know the stack of contexts at init time? Or only render time?
Both are possible, when it's a HOC it's known statically:
subscribe([Theme, Store], (theme, store) => ({ theme, store }))(View)
But it can also be dynamic:
<Subscribe to={[Theme, Store]} pick={(theme, store) => ({聽theme, store })}>
{View}
</Subscribe>
Have you tried several utilities that exist to compose nested render props?
Even if you don't end up using them, you can check out how they're implemented.
Disclaimer: I did the first of those, and I realized it is solving the same problem you seem to have, because I recall I used reduceRight too 馃槃
I believe most of them would fail with react 16.3.1, because they probably do it the same way i do: https://github.com/drcmda/react-contextual/blob/a8d35863a156814e86c0f82af1eb357fad89c401/src/subscribe.js
No. I believe none of them do any mutations in the process, as you seem to need to do in here. I know at least mine does not do any mutation at all (it used to do it in the initial implementation but it is now purely pure, pun intended).
Very interesting, thanks a lot, i'll see if this works.
Looks like the version without mutation works.
const consumers = [context1, context2].reduceRight(
(inner, ctx) => (...args) => (
<ctx.Consumer>
{value => inner(...args, value)}
</ctx.Consumer>
),
(...values) => <View values={values} />
)();
Given your example with
<context1.Consumer>
{value1 =>
<context1.Consumer>
{value2 =>
<context1.Consumer>
{value3 => `${value1} ${value2} ${value3}`}
</context1.Consumer>
}
</context1.Consumer>
}
</context1.Consumer>
If you鈥檙e curious how I unrolled it (or, rather, rolled it?):
Note: I don't recommend anyone to write code like this in applications, it's really hard to read. Might be handy for some very specialized libraries.
Incredible! Thanks so much! https://github.com/drcmda/react-contextual/blob/master/src/subscribe.js#L28
Need to study more functional programming ... i couldn't have done this in a hundred years 馃槗
If it鈥檚 easier to do with a loop then use a loop 馃檪 I don鈥檛 think it鈥檚 necessary to use reduce in every case.
If you list each context in a separate statement like I did then it should become visible what the loop body should be like. Then reduce vs loop is only a choice of form.
Hi, late to the discussion, but I think i figured it out (being totally nerd-sniped here).
The mutation in render is not the cause here (not saying it is safe either). The real cause is the React elements being reused; if they are recreated, it works https://codesandbox.io/s/ql0592xxnq (I added just another lambda + calls).
Is this an expected behavior, not to update the consumer if the element is the same, even though the closed value may change?
EDIT: reordered the reducer to make sure that each render is created only once https://codesandbox.io/s/018lo88r2p
EDIT2: This is more related to render props, minimal example: https://codesandbox.io/s/01vm6v1qrv
EDIT 3 and last: "Stale" consumers are updated in alpha, but not in stable (other elements need to be new to be updated in both versions): https://codesandbox.io/s/4834pp4634
@redwormik thanks, i suspected it's the accumulator not getting updated, but my mind was stuck on context.consumer. The last example isolates it nicely, @gaearon is this known/expected behaviour?
@gaearon I think I have a similar problem but I'm not sure: my Provider is not updating Consumer/s.
I fully reproduced it here: https://codesandbox.io/s/ry86mlylmm
At Router.js:4 I define my context with a redirect
prop equal to a noop (Function.prototype).
At Router.js:22 I'm setting my context redirect
prop equal to Router.redirect method and I'm attaching it to the state.
At Router:46 I set the context.value = Router.state thus hoping my Consumers will re-render when I'm updating it.
At Router:22 (in componentDidMount) the state is updated and I can see the Router re-rendering (console logging).
But Consumers are not, withRouter.js:12 never logs the re-render!
What's wrong with my Context?
@damianobarbati It looks like your <Link />
components are rendered outside of the router context (see Layout.js and index.js). Currently as you have it the Nav component is rendered above the Router, meaning the Links will fallback to the default value passed to createContext
.
Thanks @hamlim, that solved! 馃憤馃徎
Most helpful comment
Given your example with
If you鈥檙e curious how I unrolled it (or, rather, rolled it?):
Note: I don't recommend anyone to write code like this in applications, it's really hard to read. Might be handy for some very specialized libraries.