reactjs / rfcs

RFCs for changes to React
MIT License
5.53k stars 558 forks source link

How do this work when you need abstraction? #24

Closed srcspider closed 6 years ago

srcspider commented 6 years ago

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.

trueadm commented 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)

amrdraz commented 6 years ago

@srcspider are you referring to context?

srcspider commented 6 years ago

@amrdraz yes, see discussion in #23

bvaughn commented 6 years ago

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:

Your Component X library

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>
);

Code that uses your library

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>
srcspider commented 6 years ago

@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

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."

TrySound commented 6 years ago

@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>
  </>
)
bvaughn commented 6 years ago

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.

srcspider commented 6 years ago

@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.

srcspider commented 6 years ago

Closing based on clarification from @gaearon in #23

bvaughn commented 6 years ago

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>
    );
  };
}