Closed srcspider closed 6 years ago
What's this comment in relation to? I don't think you meant to make an issue here? (Same as the other issue you made)
@srcspider are you referring to context?
@amrdraz yes, see discussion in #23
I'm a little unclear on what you're asking here too. Terms like "component x" and "thing a" are a little too ambiguous. But I think you're asking how you could write a library with a component that consumes a context value that must be provided by users of your library. In that case, you could do something like this:
const ThingAContext = React.createContext(null);
// Export the provider for libraries using Component X.
// These libraries are required to supply their own context value.
export const ThingAProvider = ThingAContext.Provider;
// Export Component X for use however it is intended.
export const ComponentX = props => (
<ThingAContext.Consumer>
{thingA => {
if (thingA === null) {
throw Error('You must provide a value for Thing A');
}
return (
<SomeInternalComponentThatRequiresThingA
{...props}
thingA={thingA}
/>
);
}}
</ThingAContext.Consumer>
);
import {ComponentX, ThingAProvider} from 'some-component-x-library';
// I need to specify my own value
<ThingAProvider value="My own value for thing A">
<div>
<div>
<ComponentX foo={123} bar="abc" />
</div>
</div>
</ThingAProvider>
@bvaughn
This is clearly getting us nowhere fast. I've given an answer to your post at the end, but the short of it is that it's not what I meant.
To make this "problem" as simple as possible I've boiled it down to a "fizzbuzz"-like problem instead. This way it's very simple and we just have to talk code. Solve the problem within the given limitations and any confusion goes away.
Template
https://jsfiddle.net/54s6p6ux/
class Player extends React.Component {
render() {
let chosenColor = undefined;
return (
<div>Player chose color {chosenColor}</div>
);
}
}
// This simulates there being a boundry betweent the Player component
// and the game component. If there was none the obviously there is no
// need for a "context" system of any sorts at all!
function Chaos(/* no arguments allowed! */) {
let comp = <Player/>;
let randomTimes = Math.floor((Math.random() * 10) + 1);
while ( --randomTimes > 0) {
comp = <div>{comp}</div>
}
return comp;
}
class Game extends React.Component {
render() {
return (
<React.Fragment>
<h2>{this.props.name}</h2>
<p><b>Game Colors</b>: {this.props.colors.join(', ')}</p>
<Chaos/><Chaos/><Chaos/>
</React.Fragment>
);
}
}
for (let i = 1; i <= 100; ++i) {
let colors = [];
for (let j = 0; j < 10; ++j) {
colors.push(`c${i}-${j}`);
}
let container = document.createElement('div')
document.body.appendChild(container);
ReactDOM.render(
<Game name={`Game ${i}`} colors={colors} />,
container
);
}
Limitations
Chaos
function at all!Chaos
function is allowed to work with Player (simulates realworld deep hierarchies)Objective
100 games are created on the page. Each game has 9 unique colors (names not important). Each game has 3 players. Each player randomly chooses one of the 9 colors.
A template with all the boring stuff is provided. You just have to fill in the code so that colors make it to the players and they print one at random.
Example
Here is the solution using the classic context system.
https://jsfiddle.net/8xqwfma1/
With regard to your example, just so it doesn't go unanswered:
The solution you provided, to my understanding has the same limitations as if I were to just have a random nodejs module exporting ThingA and every other module importing said module (you just did it with a lot more code). I would achieve the same result and suffer from the same limitation, that being that it is inherently a singleton context. I never want singleton contexts. Even if it is a single ThingA in the application, having it as a singleton means, "there can never be anything ThingA on the page."
@srcspider New context api is not singleton. Data you pass is kept in the tree. You may add many sibling providers and every consumer will consume data of only own provider.
const Cx = createContext(0);
const Component = () => (
<>
<Cx.Consumer>
{value => value /* 0 */}
</Cx.Consumer>
<Cx.Provider value={1}>
<Cx.Consumer>
{value => value /* 1 */}
</Cx.Consumer>
</Cx.Provider>
<Cx.Provider value={2}>
<Cx.Consumer>
{value => value /* 2 */}
</Cx.Consumer>
</Cx.Provider>
</>
)
Every time your Chaos
component is rendered, it will produce different outputs. This is not how React components are supposed to work.
That being said, here's how you could create the same "game" you've shown using the new context API: https://codesandbox.io/s/24y59mj0jn
The solution you provided, to my understanding has the same limitations as if I were to just have a random nodejs module exporting ThingA and every other module importing said module (you just did it with a lot more code). I would achieve the same result and suffer from the same limitation, that being that it is inherently a singleton context. I never want singleton contexts. Even if it is a single ThingA in the application, having it as a singleton means, "there can never be anything ThingA on the page."
No, although I guess I can see how you came to that conclusion. Maybe this example helps clarify?
import {ComponentX, ThingAProvider} from 'some-component-x-library';
<>
<ThingAProvider value="foo">
<ComponentX /> <!-- gets "foo" as "thing a" -->
<ThingAProvider value="bar">
<ComponentX /> <!-- gets "bar" as "thing a" -->
</ThingAProvider/>
</ThingAProvider>
<ThingAProvider value="baz">
<ComponentX /> <!-- gets "baz" as "thing a" -->
</ThingAProvider>
<ComponentX /> <!-- gets null as "thing a" -->
</>
The context API supports overriding values via nested providers.
@bvaughn
That being said, here's how you could create the same "game" you've shown using the new context API: https://codesandbox.io/s/24y59mj0jn
The solution is wrong because you manipulated the Chaos function (see Limitations section in initial post). Chaos represents parts of the code you don't manipulate (such as 3rd-party intermediate components that sit between your root "Game" component and your "Player" component, in this case).
Based on what TrySound posted, the "solution" is this https://codesandbox.io/s/98jrv09qvw
@TrySound
@srcspider New context api is not singleton. Data you pass is kept in the tree. You may add many sibling providers and every consumer will consume data of only own provider.
Well I'm happy to be proven wrong. And you're right I was thiking that due to how simplistic it looks (all blog posts about it so far don't touch on it either I think).
But at the same time I don't feel too good by how complicated it seems to actually be internally. The fact it can't be used in the constructor or any function outside of render is a big led down still. I'm to assume that the reason it's not able to pass it directly to the constructor or some componentDidRecievedContext
is not just some style choice? and has something to do with limitations behind the logic of workLoop
, beginWork
and updateContextConsumer
in React's internals?
Seems I'm doomed to have to create 3 separate components each time I need to create one.
But again thanks for clearing up it's not a singleton pattern.
Closing based on clarification from @gaearon in #23
The solution is wrong because you manipulated the Chaos function (see Limitations section in initial post).
Right. I arbitrarily placed the Context.Consumer
component within the Chaos
component because (in the isolated example) it seemed to make sense. You can move the consumer into Player
if you prefer, or anywhere between the Context.Provider
and Player
.
Updated example at: https://codesandbox.io/s/j2ny0xnw23
The fact it can't be used in the constructor or any function outside of render is a big led down still.
For anyone else who sees this thread in the future, this comment on issue #23 explains how you can access context values in the constructor (or elsewhere outside of render
). You just need to lift the consumer up a level.
For example, "lifting up" can be done with a higher order function, like so:
function withColorsContext(Component) {
return function ColorsContextConsumer(props) {
return (
<ColorsContext.Consumer>
{colors => <Component {...props} colors={colors} />}
</ColorsContext.Consumer>
);
};
}
Scenario: You have Component X. Component X is a generic component in a "library" (assume this is a 1st-party shared-code style library, not 3rd party; ie. some git submodule, etc). Said component wants to be provided a certain well defined per-project entity "ThingA".
How does this Consumer/Provider relationship work when you want each project to be able to provide their own ThingA that guarantees the interface required by Component X but is not limited to it?
How do you make it so Component X has access to it's required Consumer, but said Consumer is required to be inside the library, and each project is free to create their own Provider that satisfies the Consumer for X, but said Providers are required to be outside the library.
Extra Optional Assumption: Assume that primarily the responsibility of Component X when using the context provided is to bridge functionality of the project, so the surface interface is consistent but the relationship is primarily in the sense of project passing project specific logic down for it to be called back (by Component X) to it in some other part of the project specific code.