New version of context #2

Open
wants to merge 4 commits into
from

Conversation

Member

acdlite commented Dec 6, 2017

(supersedes #1)

A proposal for a new version of context addressing existing limitations.

Rendered

Corresponding React PR: facebook/react#11818

TODO:

  • Implementation notes
  • High-priority version of context, for animations. (May submit this separately.)
  • Add snippet showing how the displayName transform would work (#2 (comment)).
  • Add section about using narrow context types for maximum performance

acdlite added some commits Dec 6, 2017

RFC: New version of context
A proposal for a new version of context addressing existing limitations.

@acdlite acdlite referenced this pull request Dec 6, 2017

Closed

RFC: New version of context #1

0 of 2 tasks complete

@mjackson compareValues is a good idea. What's the default comparison in RB? ===?

Member

acdlite commented Dec 6, 2017

@jaredpalmer What's the use case for compareValues? I don't see one except to make mutation easier, which we're specifically aiming to discourage.

Owner

mjackson commented Dec 6, 2017

@jaredpalmer Yep. === is the default.

@acdlite There's a difference between discouraging mutability and making it impossible to use, IMO. Seems like exactly what defaultProps are for: by default we'd prefer you don't mutate stuff. But if you need to, just override compareValues. AFAICT immutability isn't actually enforced anywhere else in React. One use case is passing around location objects from the current history API, which is mutable.

Member

acdlite commented Dec 6, 2017

@mjackson Mutation is not impossible with this proposal, it's just slightly less convenient. You can mutate one level down, just not the outermost container. Same as setState API.

jaredpalmer commented Dec 6, 2017

Just giving an escape hatch to fallback to. I can see why this should be discouraged. However, mutation did come up in Formik the other day where I wanted to alter something within context if a certain declarative component existed in the subtree.

Edit: Ahh I see now that you can mutate one level down

Member

acdlite commented Dec 6, 2017

@mjackson

AFAICT immutability isn't actually enforced anywhere else in React.

Historically we have tried to be as forgiving of mutation as possible. When we introduce async rendering (this proposal is designed with async in mind), we may need to be a bit more restrictive.

However, there are cases where React already privileges immutability. setState is one. Another is that we bail out on React elements if the props objects are strictly equal (===). Often that isn't the case, because props objects are created in the render method as part of JSX / React.createElement. But it is the case for components that render this.props.children. If the component re-renders, but the parent didn't, the child elements will bail out on the unchanged props.

+
+# How we teach this
+
+This would be our first API that uses render props. They are an increasingly
@ljharb

ljharb Dec 6, 2017

"render props" and "function children" are different; no matter how they're implemented, children aren't just a normal prop - they're special (the third argument to createElement wins over a "children" prop, for example).

Please don't encourage function children.

@paularmstrong

paularmstrong Dec 6, 2017

Please don't encourage function children.

@ljharb Can you explain why this shouldn't be encouraged?

@pshrmn

pshrmn Dec 6, 2017

@paularmstrong One argument would be that children is almost always used as an implicit prop, but children as a function encourages using children as an explicit prop, which will lead to a bugs when someone attempts to use both at the same time.

<Thing children={firstFn}>
  woops
</Thing>
React.createElement(Thing, { children: firstFn}, 'woops')
{
  type: Thing,
  props: { children: 'woops' },
  ...
}

Yes, that can already happen, but I've never seen anyone write something like this:

<Thing
  children={
    <Well>
      <This is='awkward' />
    </Well>
  }
>
  ...
</Thing>
@kentcdodds

kentcdodds Dec 7, 2017

FWIW, @ryanflorence convinced me that it's easier to teach people a render prop accepts a function than it is to teach folks that the children prop can be a function. So in my teaching and open source I've been moving things over to render exclusively to hopefully make things more cohesive across the community while making it easier for people to pick up.

So, I'm in favor of using a prop called render personally.

@dmiller9911

dmiller9911 Dec 7, 2017

I completely agree with using render prop instead of children. In any instruction/demos I have given it is much easier for people to wrap their head around a prop called render over children being a function.

@ljharb

ljharb Dec 11, 2017

@jedwards1211 the fact that a component has the choice to render its children (or any prop) or not, does not change that children are special. They're not special because they're proxied through; they're special because of how you pass them in the first place.

@philholden

philholden Dec 12, 2017

I prefer render props over Function as Child for one main reason it leads to less indentation and less lines of code:

FaC: 5 lines, indent x2

<MyComponent>
  {
    x => <div>{x}</div>
  }
</MyComponent>

render props 3 lines, indent x1:

<MyComponent render={
  x => <div>{x}</div>
}/>

I gave up on FaCs and reverted to HoCs mainly because the level of indentation needed made the less readable even though FaCs are more explicit.

@philholden

philholden Dec 12, 2017

Specially bad when nested:

FaC:

<MyComponent>
  {
    x => {
      <MyComponent2>
        {
          y => <div>{x}{y}</div>
        }
      </MyComponent2>
    }
  }
</MyComponent>

renderProps:

<MyComponent render={
  x => <MyComponent2 render={
    y => <div>{x}{y}</div>
  }/>
}/>
@gaearon

gaearon Dec 12, 2017

Owner

That just depends on how you format this. You don’t have to indent it an extra time.

<MyComponent>
  {x => (
    <div>{x}</div>
  )}
</MyComponent>

That's how Prettier formats it too.

@bvaughn

bvaughn Dec 12, 2017

Collaborator

By default, Prettier formats both as 1-liners 😄

<div>
  <MyComponent>{x => <div>{x}</div>}</MyComponent>
  <MyComponent render={x => <div>{x}</div>} />
</div>;
+## Add displayName argument to createContext for better debugging
+
+For warnings and React DevTools, it would help if providers and consumers
+had a `displayName`. The question is whether it should be required. We could
@ljharb

ljharb Dec 6, 2017

If they have a function name, they don't need a displayName - perhaps making getComponentName(…) required, but not displayName specifically?

@gaearon

gaearon Dec 6, 2017

Owner

My original point is that with this API (unlike existing one) we don’t know which context is missing to show a meaningful warning. Passing a name (explicitly or implicitly) to createContext would solve that.

@ljharb

ljharb Dec 6, 2017

Totally fair, I'm 100% fine with requiring a name - just not requiring displayName specifically, since normal function names should be more than sufficient.

@gaearon

gaearon Dec 6, 2017

Owner

createContext() isn’t bound to any component or function in particular so it can’t read a .name from it. Therefore my proposal is to make it an argument (even if optional). I think that’s what displayName was referring to. For example:

const Theme = React.createContext('light', {
  displayName: 'Theme’
});

Sorry if I’m missing your point.

@acdlite

acdlite Dec 7, 2017

Member

Another option is

const Theme = React.createContext('light');
Theme.displayName = 'Theme';
@ljharb

ljharb Dec 8, 2017

@gaearon ah, I misunderstood. In that case, yes, I'd say requiring displayName makes perfect sense.

@Andarist

Andarist Dec 9, 2017

Static patterns like:

const Theme = React.createContext('light');
Theme.displayName = 'Theme';

can often prevent tree-shaking/dead code elimination. I'd advise not to recommend this officially, better to have an optional extra param for createContext + maybe auto add static displayName with babel plugin for the development purposes.

+
+## Other
+
+* Should the consumer use `children` as a render prop, or a named prop?
@ljharb

ljharb Dec 6, 2017

named prop, please. children are special, and should only be nodes.

@jaredpalmer

jaredpalmer Dec 6, 2017

+1 for render

@jmeas

jmeas Dec 7, 2017

children are special, and should only be nodes.

Would you mind elaborating on this a little more?

One thought I have is that a new prop could be considered as API bloat if children already functions exactly as it needs to. I'm not convinced one way or the other on this one, by the way; I'm just sharing one more thing worth considering.

@JNaftali

JNaftali Dec 7, 2017

The type signature for the children prop is pretty consistent across all built-in React components (that I can think of) and most components shipped with third party libraries - Component | SFC | string | etc. The two big exceptions that I've seen are components that don't support children at all and components that require a single child component (React Router's FooRouter components spring to mind).

In particular I think the distinction between (foo) => <DomElement /> vs ({foo}) => <DomElement /> and {renderProp} vs <FunctionalComponent /> is a fine one. I could see that getting confusing in particular if the render prop function isn't defined inline.

All that is why I prefer an explicit 'render' prop, anyway. My knowledge of the React ecosystem is not nearly as in-depth as many of yours' - are there lots of big exceptions that I'm not thinking of?

Edit to add: this is all a user's perspective, not a package author's. If you're still gonna try and discourage users from using the new Context API all of the above might be considered a plus.

@jmeas

jmeas Dec 7, 2017

That makes sense to me, @JNaftali . I’m now leaning more in favor of a separate prop as well.

@stryju

stryju Dec 8, 2017

You can always pass children as prop:

<Foo children={() => {...}} />
@jedwards1211

jedwards1211 Dec 10, 2017

@ljharb Components have always been free to do whatever they want with children. I would agree with you if React by design always passed through children unaltered, but there are so many library components out there that conditionally render children, render them inside wrappers (e.g. interactive rearrangeable grids), etc. that I think "children are special" is a dangerous argument that leads to restricting the power of React.

@jedwards1211

jedwards1211 Dec 10, 2017

also, saying that children should only be nodes because they are "special", or because the third argument to createElement wins over a "children" prop, are non-arguments. They don't really explain what if anything you consider cleaner, easier to read, easier to understand, less error prone, etc. about ensuring that children are only nodes.

@JNaftali

JNaftali Dec 10, 2017

I don't see how a convention could possibly 'restrict the power of React'. Conventions don't stop you from doing anything. They're just a tool to make it easier for other people to understand how to use the things you write. React's power is such that given any one of these APIs it'd be easy to implement any of the others that will remain true no matter which of these APIs get implemented.

@jedwards1211

jedwards1211 Dec 10, 2017

That's true, it's not clear to me if @ljharb is just advocating this as a convention or if he would be in favor of dropping support for function children from React.

As far as making things easier to understand...personally I find child functions more obvious than render functions in other props, but I guess I'm in the minority.

+## Other
+
+* Should the consumer use `children` as a render prop, or a named prop?
+* How quickly should we deprecate and remove the existing context API?
@ljharb

ljharb Dec 6, 2017

very slowly. Separately, it must be possible to write a component that can work, simultaneously, on old React (with current context) and new React (with new context).

@acdlite

acdlite Dec 6, 2017

Member

That's a good point about libraries needing to support both APIs at the same time

@gaearon

gaearon Dec 7, 2017

Owner

Implementation wise could we only leave the new implementation but polyfill the old API on top of it (with current broken semantics)?

@acdlite

acdlite Dec 7, 2017

Member

Ooh maybe! Should at least consider it.

@ljharb

ljharb Dec 7, 2017

As long as I can trivially author a package that can work in both, I consider this addressed :-)

We didn't use context that much and decided to use Redux instead.

To provide some inputs, I scanned one module. If it helps:

  • out of our 19 redux-connected components, we have 12 components that have only one value selected from the state, aka one selector. Mostly, isLogged, isLoading or isOnline. This proposal totally covers this case elegantly.
  • The remaining 7 components have an average of 4 selectors, to a max of 6. Reading the code, they all seem legitimate to me but we may have got it wrong: isLogged, isOnline, isLoading, getItemsPage, getTotalOfItems, getTheme.
    The new context still sounds laborious in that case.
Owner

gaearon commented Dec 6, 2017

To be clear even if you use Redux, React Redux still uses context under the hood. The problem is it has to reimplement a tree-aware subscription mechanism. And that won’t work very well with React async mode. The proposal is both about making context friendlier for direct use and to make it better for libraries built on top (such as React Redux).

milesj commented Dec 6, 2017

(Copied from previous PR)

I think a life-cycle hook in regards to context changes should be discussed. Relying on a child function for detecting this isn't exactly feasible for all situations. Perhaps another prop on the consumer?

<Consumer onChange={this.handleChange} />
Member

jlongster commented Dec 6, 2017

@milesj brings up a good point. It took me a bit to parse his question but I think he's getting at this: what if you need the context value in any of the lifecycle hooks?

The problem with a render prop API is it makes that very difficult; I've run into this problem with other render prop-based APIs as well. Take AutoSizer which gives you the width/height. I needed to do something special with the width to figure out if I should render something or not, and that logic needs to be re-run when something else changes in a lifecycle, so I need the width there. Hope that makes sense, I can't remember the details of the problem.

Is the suggested pattern to pull out all the lifecycle code that needs to depend on it into a sub-component, and pass the value down as a prop to it? I think that works but seems a bit ceremonious.

Member

acdlite commented Dec 6, 2017

@jlongster

Is the suggested pattern to pull out all the lifecycle code that needs to depend on it into a sub-component, and pass the value down as a prop to it? I think that works but seems a bit ceremonious.

Yes, this is the main problem with render prop APIs. I think this is mostly an educational challenge, though, because there's always the option to wrap it in an HOC:

function consume(Context) {
  return Component => {
    return function Consumer(props) {
      return (
        <Context.Consumer>
          {context => <Component context={context} {...props} />}
        </Context.Consumer>
      );
    }
  }
}
Member

acdlite commented Dec 6, 2017

Usually my objection to wrapping in an HOC, versus using an HOC from the start, is it requires two extra components instead of one. But in this case, we want the extra Consumer component because they're faster to scan for.

Member

jlongster commented Dec 6, 2017

Faster to scan in what case? Is that benefit enough to warrant a HOC in the default API?

(Thanks for explaining!)

Member

acdlite commented Dec 6, 2017

Faster to scan for Consumer components. By adding another node into our internal tree, with a special type that is distinct from class components, we can quickly skip over non-consumer nodes.

To be clear, we could still implement it this way with an HOC-first API. I was just rebutting my own devil's advocate point about adding extra nodes to the tree :D

EDIT: Realized I misread your question. Faster to scan for Consumer components when a provider changes. We don't do this every time there's a change, only on the first update. Then the result is cached. We may have to re-scan if the cache is cleared.

Member

acdlite commented Dec 6, 2017

I'm working on adding a high-level description of the implementation, will try to publish later today or tomorrow.

Will the new API support provider nesting like we can do it today? One use case example is nested theme.

Member

acdlite commented Dec 6, 2017

@oliviertassinari Yes

milesj commented Dec 7, 2017

@jlongster That's half of the question. But basically, I think the new context would need to hit these points:

  • New life-cycle event(s) for when the context changes (componentWillReceiveContext)? Passing the context prop to a component, in which that component needs to componentWillReceiveProps, adds another layer of abstraction and feels very tedious/contrived. My rudimentary solution above was simply a prop handler for when the context changes, instead of a new life-cycle method.
  • Checking the context value in existing life-cycle events. Theoretically this can be achieved by accessing the context prop.
+container. (Or even just alternate between two copies.) React will detect a new
+object reference and trigger a change.
+
+## Only one provider type per consumer
@kentcdodds

kentcdodds Dec 7, 2017

Someone will inevitably develop a component to do this composition automatically. I'm not worried about this drawback 👌

@sprjr

sprjr Dec 7, 2017

Agreed, and while I'm not super excited about the (below) suggested layering I'm certain that if one wasn't available I'd simply make one for use in apps to layer them together ala <ApplicationKitchenSinkConsumer> or something like that.

@baptistemanson

baptistemanson Dec 7, 2017

I agree with you about the tooling, it will happen because it is necessary.
Inspectors, stack trace and performance may be impacted by the layering though.

@kentcdodds

kentcdodds Dec 7, 2017

I honestly doubt this will happen all that often... 🤷‍♂️

@acdlite

acdlite Dec 7, 2017

Member

Also consider that we can change the DevTools to display this however we like

@baptistemanson

baptistemanson Dec 7, 2017

@acdlite Indeed! DevTools improvements for HOC/layering makes sense beyond this RFC I think.

@kentcdodds I grep'ed the code of our apps this afternoon to review the impact of this RFC. We only have about 25 developers working with React, so consider my feedback with a grain of salt - we do not have the same stats as Facebook has :).
Our login button render method changes when logged or not authenticated (login or logout), on the theme (color), on being connected to the network or offline (disabled) and on the language of the user.

Overall, 20% of our context dependent render methods read 2 or more "atomic global state variable". If I understood correctly, the RFC suggests that if we want to only use React with no 3rd party tool, we should use 1 Context Consumer for each Context Providers we want to "consume from" - it sounds inconvenient to me.
But we already tooled ourselves with Redux and the RFC doesn't degrade our Redux experience so if it solves problems for others, go!

What about your React applications?

@kentcdodds

kentcdodds Dec 7, 2017

Most applications don't use the context API directly in every component that needs it and instead use an abstraction of some kind (HOC, render prop, or something else) to access context. In this case, the impact of the APIs verbosity is pretty minimal and the benefits of the new API make it easily worth it.

+
+# How we teach this
+
+This would be our first API that uses render props. They are an increasingly
@kentcdodds

kentcdodds Dec 7, 2017

FWIW, @ryanflorence convinced me that it's easier to teach people a render prop accepts a function than it is to teach folks that the children prop can be a function. So in my teaching and open source I've been moving things over to render exclusively to hopefully make things more cohesive across the community while making it easier for people to pick up.

So, I'm in favor of using a prop called render personally.

+
+This would be our first API that uses render props. They are an increasingly
+popular pattern in third-party React libraries. However, there could be learning
+curve for beginners.
@kentcdodds

kentcdodds Dec 7, 2017

Can't deny the learning curve. But to be fair, there's a learning curve (and arguably a steeper one) with the current context API. In addition, I think people will start to see render prop APIs before they have to start using context themselves. So it's unlikely that people will come to the context API before they've experienced a similar API with other libraries first.

@karlbright

karlbright Dec 7, 2017

Agreed with @kentcdodds, context has a learning curve. I think beginners coming across the proposed context API, making use of a render prop, will have a much easier time grokking what is happening, or where to start with it.

@brentmclark

brentmclark Dec 7, 2017

The bigger learning curve for the existing context API is trudging through myriad opinions about whether or not it's a good idea to use it, and, if so, how to use it safely.

This proposal being slightly more opinionated seems like it would result in a more gradual learning curve.

+
+For warnings and React DevTools, it would help if providers and consumers
+had a `displayName`. The question is whether it should be required. We could
+make it optional, and use a Babel transform to add the name automatically. This
@kentcdodds

kentcdodds Dec 7, 2017

Could we make it required, but have the babel plugin add the displayName in the function call automatically? Best of both worlds?

const ThemeContext = React.createContext('light')

↓ ↓ ↓ babel plugin ↓ ↓ ↓

const ThemeContext = React.createContext('light', {
  displayName: 'ThemeContext',
})
@acdlite

acdlite Dec 7, 2017

Member

Yeah, that's what I was trying to communicate. Maybe I'll just copy your snippet :D

I was thinking the displayName property would be assigned to the context object directly, as with other component types:

const ThemeContext = React.createContext('light')

↓ ↓ ↓ babel plugin ↓ ↓ ↓

const ThemeContext = React.createContext('light');
ThemeContext.displayName = 'ThemeContext';
@kentcdodds

kentcdodds Dec 7, 2017

Sounds fine. I thought making it part of the createContext call would make it easier to log an error. I don't really care how it's implemented though 😅

@Andarist

Andarist Dec 9, 2017

If you want to make the plugin to output static property, please make it in such fashion

const ThemeContext = React.createContext('light');
if (process.env.NODE_ENV === 'development') {
  ThemeContext.displayName = 'ThemeContext';
}

Otherwise statics will prevent tree-shaking. This could be configurable with wrap option for the transform which would default to true

@gaearon

gaearon Dec 9, 2017

Owner

Or, rather,

const ThemeContext = React.createContext('light');
if (process.env.NODE_ENV !== 'production') {
  ThemeContext.displayName = 'ThemeContext';
}

to match how we do other checks.

+ return (
+ // The Consumer uses a render prop API. Avoids conflicts in the
+ // props namespace.
+ <ThemeContext.Consumer>
@sprjr

sprjr Dec 7, 2017

I noticed a few of our internal apps started using redux in a way such that they do not connect leaf components but instead just rely on the store being accessible on this.context and they grab things from there directly. I'm not saying this is a recommended approach just that it is one I've seen taken.

Given that behavior (or other direct context access and not using a context -> props hoist component) what would be the migration story for apps wanting to update and needing to replace that with the new <ThemeContext.Consumer>?

I realize that's a bit of a loaded question, but it seems like it wouldn't necessarily be something that could be code shifted at all, I imagine.

@baptistemanson

baptistemanson Dec 7, 2017

Intuitively, the migration to using connect()+props instead of reading straight out of context would be my first investigation.
A grep like:

$grep -hr this\.context src/

should give you an idea of the amount of effort required for this direction. Hope that helps!

@karlbright

karlbright Dec 7, 2017

Discussion around how long existing API would hang around, and implementation for that can be found #2 (comment)

@acdlite

acdlite Dec 7, 2017

Member

If you give a static value (like store) to a context provider, consuming it in a child is basically free. So you can continue to use it for the this.context.store use case. The only difference is you'll have to use the render prop, instead of accessing it on the instance. Unfortunately this means there's no obvious way to codemod this, but it shouldn't be hard for a human to do the migration.

suchipi commented Dec 7, 2017

It might be nice for React to ship an "official" HOC for consuming Context in the lifecycle, so that it can be treated differently in React DevTools (different color or etc). It's already inconvenient to dig through a ton of layers of components when using context-based APIs; needing to add another layer to get context in lifecycle methods will exacerbate the issue.

+static values is still supported.
+
+Replacing subscription-based patterns in favor of context's built-in change
+propgation may require a larger effort. However, as a first step, developers can
@jmeas

jmeas Dec 7, 2017

small typo: propagation

+the previous context, and return true if they are different. The problem is
+that, unlike props or state, we have no type information. The type of the
+context object depends on the component's position in the React tree. You could
+perform a shallow comparion of both objects, but that only works if we assume
@jmeas

jmeas Dec 7, 2017

small typo: comparison

(if you plan to do a spellcheck at some point, no worries; I can stop pointing these out. I'm just reading through this document and pointing them out as I find them. By the way, this is great so far! Thank you! ✌️ )

@acdlite

acdlite Dec 7, 2017

Member

Ha your spellchecks are appreciated :)

What's the suggested story for when a library consumes a context that is created and provided by an application? No particular usecase in mind yet, but previously this was possible via naming convention because there was a single context object.

suchipi commented Dec 7, 2017

@sompylasar I guess you'd pass the context consumer into the library?

import makeButton from "button-lib";

const Theme = React.createContext("light");

const Button = makeButton({ themeConsumer: Theme.Consumer });

<Theme.Provider value="dark">
  <Button />
</Theme.Provider>

jmeas commented Dec 7, 2017

@sompylasar , one idea that comes to mind is that the library could only accept props, and then create its own context as needed.

// In your app
<AppComponent>
  <SomeContext.Consumer>
    {val => (<SomeLib val={val})}
  </SomeContext>
</AppComponent>
// In SomeLib
export default function SomeLib({ val }) {
  return (
    <LibContext.Provider val={val}>
      <InternalLibStuff/>
    <LibContext/>
  );
}

This would allow you to place the value on context in your application as well as in the lib without needing to pass around consumers, although maybe directly passing in the Consumer like @suchipi suggested would be preferable.

suchipi commented Dec 7, 2017

(Or you could pass it as a prop instead of the HOC approach, etc)

Sometimes, when I read a value that stored in context I also need handler(s) to update this value. E.g.:

getChildContext = () => ({
  locale: this.state.locale,
  updateLocale: this.updateLocale,
});

/* application */
<LocaleProvider>
  {({ locale, updateLocale }) => ... }
</LocaleProvider>

AFAIU from this RFC Consumer serves only value so handlers still must be passed all the way down?

TrySound commented Dec 7, 2017

@alexfedoseev Nothing stops you to pass handlers via context

<Provider value={{ locale, updateLocale }}>
  <App />
</Provider>

/* application */
<Consumer>
  {({ locale, updateLocale }) => ... }
</Consumer>

@suchipi @jmeas Right, thanks, these are both the options I thought about, too. I thought maybe there's something that we're missing.

alexfedoseev commented Dec 7, 2017

@TrySound Right, my bad! I was confused by value name lol. It's probably more like interface/payload or something. Thanks for clarification!

jeffijoe commented Dec 7, 2017

About the render-prop vs function-as-child discussion; I tried writing some nested consumers in both formats, then plop them in Prettier.

I think the function-as-child approach is preferable; it results in less indentation and less lines.

// Function as child
const App = () => (
  <div>
    <Foo.Consumer>
      {value => (
        <div>
          {value}
          <Baz.Consumer>
            {value => (
              <div>
                {value}
                <Qux.Consumer>{value => <div>{value}</div>}</Qux.Consumer>
              </div>
            )}
          </Baz.Consumer>
        </div>
      )}
    </Foo.Consumer>
  </div>
);

// Render prop
const App = () => (
  <div>
    <Foo.Consumer
      render={value => (
        <div>
          {value}
          <Bar.Consumer
            render={value => (
              <div>
                {value}
                <Baz.Consumer
                  render={value => (
                    <Qux.Consumer render={value => <div>{value}</div>} />
                  )}
                />
              </div>
            )}
          />
        </div>
      )}
    />
  </div>
);

JNaftali commented Dec 7, 2017

The above is actually why I would like to see an HOC interface that comes from React and uses the render prop interface under the hood - both of those seem very difficult to parse at a glance.

Also, I imagine folks who have used jQuery in the past might find so many nested callbacks off-putting. Didn't Promises get added to JS almost entirely so people could stop seeing the Pyramid of Doom? (Kidding, you don't need to explain the value of promises to me. Probably.)

Note from a maintainer: edited down the tone a little bit. Let’s avoid unnecessary generalizations.

Owner

gaearon commented Dec 7, 2017

I wonder if making context easier to use will cause people to use it all over the place instead of prop passing because it’s less code. I wonder at which point, it ever, this actually becomes slower than prop passing and re-rendering everything.

+abstraction without involving each intermediate. Examples include a locale, or a
+UI theme. Many components may rely on those but you don't want to have to pass a
+`locale` prop and a `theme` prop through every level of the tree.
+
@JNaftali

JNaftali Dec 7, 2017

Is the motivation here to make the Context API more accessible to end-users of React, or library authors?

jeffijoe commented Dec 7, 2017

@JNaftali you're absolutely right that both nested versions are not things of beauty, haha 😄

I would imagine some sort of consumer merging utility to make it a little prettier, such as:

const CompositeConsumer = mergeContextConsumers({
  consumer1Value: Consumer1,
  consumer2Value: Consumer2,
  consumer3Value: Consumer3
})

const App = () => (
  <CompositeConsumer>
    {({ consumer1Value, consumer2Value, consumer3Value }) =>
      <div>
        {consumer1Value} {consumer2Value} {consumer3Value}
      </div>
    }
  </CompositeConsumer>
)
+```
+
+Providers and consumers come in pairs—for each provider, there is a
+corresponding consumer.
@bvaughn

bvaughn Dec 7, 2017

Collaborator

Couldn't there be more than one consumer?

@acdlite

acdlite Dec 7, 2017

Member

Yes, this is referring to the component type, not the instances. I can make that clearer.

@faceyspacey

faceyspacey Dec 8, 2017

I think a crucial way to look at this is:

react-redux had to succumb to a bunch of hacks/workarounds (and the aforementioned "drawbacks"). What would the perfect API for context look like to facilitate the react-redux package being written in a elegant/straightforward/sleek/idiomatic way?

Member

acdlite commented Dec 7, 2017

@gaearon

I wonder if making context easier to use will cause people to use it all over the place instead of prop passing because it’s less code

One thing we'll need to emphasize in the documentation is not to create overly broad context types. To keep the types simple. Like having separate ThemeContext and LocaleContext, instead of just one GenericContext that contains a record all your stuff.

By the way, this includes Redux. Having a single ReduxContext isn't ideal. It should be fine, probably even faster than React Redux today. But for maximum performance, the best practice is going to be scoped providers. Generally, the narrower the scope, the faster we can update. It may take a while for library authors to catch up to this recommendation, but I'm not too worried about it.

Member

acdlite commented Dec 7, 2017

I was thinking about how render props aren't that pleasant to read and write in JSX. I raised the possibility of changing JSX somehow to make it nicer.

@trueadm has a better idea: we don't use JSX.

render() {
  return ThemeContext.consume(theme => (
    <h1 style={{color: theme === 'light' ? '#000' : '#fff'}}>
      {this.props.children}
    </h1>
  ));
}

I really like this. It also neatly avoids the named prop versus children prop debate. I'll add it to the proposal.

Owner

gaearon commented Dec 7, 2017

To be super clear it would work anywhere in the tree, not just at top level.

<div>
  {ThemeContext.consume(theme => (
    <h1 style={{color: theme === 'light' ? '#000' : '#fff'}}>
      {this.props.children}
    </h1>
  ))}
</div>

I love @trueadm's idea because it allows us to solve our multicontext component issue quite elegantly:

<div>
  {compose(ThemeContext, AccessibilityContext).consume(
      (theme, accessibility) => (
         <h1 style={{color: theme === 'light' ? '#000' : '#fff', fontWeight: accessibility ? 'bold': 'normal'}}>
            {this.props.children}
        </h1>
  ))}
</div>
Owner

gaearon commented Dec 7, 2017

Regarding the name: should it just be read maybe?

Member

acdlite commented Dec 7, 2017

@baptistemanson FWIW you could build an API like that on top of JSX render props, too.

+ onClick={() =>
+ this.setState(state => ({
+ theme: state.theme === 'light' ? 'dark' : 'light',
+ }))
@dariocravero

dariocravero Dec 7, 2017

Is it intended on this new API that consumers could request/trigger a change in the provider? In this example, what if toggling the theme wouldn't be done at the point where the provider is defined but somewhere else on the app?

@jmeas

jmeas Dec 7, 2017

You can pass around a function that changes the value in a few different ways.

  1. utility file that supplies the provider and the update function
  2. putting an update function on the context
  3. any other solution you can think of to pass a function around your app 😉
@dariocravero

dariocravero Dec 7, 2017

Thanks @jmeas. I was asking because it seems like a common use case and the RFC is pretty extensive but it wasn't covering that case in particular.

Do you think it would it be worth it adding it as an example?

I imagine it would have to be passed down in the value of the Provider and for that the context's type would have to be changed as well. Would something like this be alright?

<ThemeContext.Provider
  value={{
    toggleTheme: () =>
      this.setState(state => ({
        theme: state.theme === 'light' ? 'dark' : 'light',
     }),
     theme: this.state.theme
  }}>

cc @acdlite

@acdlite

acdlite Dec 8, 2017

Member

One caveat is that when you pass an inline object to value, it will create a new object and trigger a context change every time the provider's parent re-renders. I think you'll want to avoid this.

I would suggest using a separate provider for event handlers, since they never change. That way you can pass the same object every time.

@dariocravero

dariocravero Dec 8, 2017

So would your suggestion then in this case be?

<ThemeContextToggle.Provider value={this.toggleTheme}>
  <ThemeContext.Provider value={this.state.theme}>
Member

jlongster commented Dec 7, 2017

Personally I think it would be nice for there to be a consistent style across React projects for how to do render props. I hadn't even though about not using JSX, and I like it, but now I'm wondering if many other components that use render props should do that as well. I don't think it sidesteps the debate - it adds a 3rd style!

It adds some ambiguity - it's not as clear that it is injecting a new component into the tree, instead of other methods which usually don't (think array mapping).

But it's not a huge deal. I'd like to teach people about render functions as children, and that would be clearer to people who already use it, but I get why you'd want to avoid "blessing" a certain style.

andreypopp commented Dec 7, 2017

Warning: I have a crazy idea. Actually there are several ideas.

Define context:

const theme = React.createContext('theme', {
  color: 'black',
  background: 'white',
})

(optional) You can define "derivations" out of context values (with memoization and so on possibly) so you can restrict on what parts you want tor react:

const color = theme.select(theme => theme.color)

Consumer site (new syntax, not a good one probably):

<Button color=!{color} />

This will be compiled to:

// React.createElement : (type, props, reactiveProps, ...children) => React.Element<props & reactiveProps>
React.createElement(Button, {}, {color: color})

The idea is to make React subscribe on values in reactiveProps bag and re-render corresponding components on changes.

I think the API React expects from reactiveProps-values could be generic enough not only for context but for other sources too.

New syntax is needed so that React don't need to traverse props to find reactive values, they are being put into a separate bag instead. Also it's kinda nice to have a visual cue for the places where subscriptions are going to appear.

BTMPL commented Dec 7, 2017

@gaearon

I wonder if making context easier to use will cause people to use it all over the place instead of prop passing because it’s less code

I can already see people (ab)using this and making a app-level store that's just an object with a simple api like update() which will replace state managers and end up re-rendering the whole tree.

I agree with Andrew, that this (re-rendering of all consumers) should be emphasised in the documentation and a greater emmphasis placed on scoped providers - ala Redux provider scoped to specific reducer.

I don't mind the non-JSX render prop-ish thing a whole lot, but it would be nice to have some consistency on how this stuff works (rather than having 3 ways to do similar things). If we want the community to move to a regular function call then that could work but it makes the pattern more challenging to implement if you'd like to use lifecycle hooks.

So whether it's the children prop or the render prop, I don't mind as much, but I'd prefer to stick with JSX.

TrySound commented Dec 7, 2017

In similar maker I'd prefer to be consistent with existing portals api and split "tools" from components. In my opinion it would be nice to think how api will look without jsx since part of the community may prefer vanilla js syntax.

Member

jlongster commented Dec 7, 2017

@TrySound I wrote React without JSX for years and found that children as a render function are automatically even better with vanilla js syntax. I don't think there's any problem there:

// With JSX

<Context.Consumer>
  {value => <div>{value}</div>}
</Context.Consumer>;

// Without JSX

Context.Consumer({}, value => div({}, value));

In vein of "consistent with existing portals api" (React.createPortal):

const valueContext = React.createContext('value');

const AppRoot = () => {
  return React.createContextProvider(valueContext, <App />);
}

const NeedsValue = () => {
  return React.createContextConsumer(valueContext, (value) => (<div>{value}</div>));
}

Doesn't look as type-sound as the original valueContext.Provider / valueContext.Consumer approach because the types of provider and consumer elements aren't co-located.

TrySound commented Dec 7, 2017

@jlongster Not quite.

createElement(Context.Customer, null, value =>
  createElement('div', null, value))

I wouldn't like to use context wrapper of hyperscript. And again there's nice createPortal api.

Member

jlongster commented Dec 7, 2017

I wasn't assuming hyperscript, but the standard createFactory provided by React which is how you use React without JSX. That verbosity is not the fault of this API but with the createElement API. (But we can respectfully move on, this is sidetracking into a different discussion!)

Member

acdlite commented Dec 7, 2017

@jlongster #2 (comment)

It adds some ambiguity - it's not as clear that it is injecting a new component into the tree, instead of other methods which usually don't (think array mapping).

Nested arrays are another example of something that creates a new node (fiber) internally without looking like it does. We omit those from the DevTools. We could do the same for context.

Member

jlongster commented Dec 7, 2017

@acdlite Do we want to though? Seeing the consumer boundaries seems like valuable information, but maybe you expose that information in another special way? (I saw devtools being talked about before and maybe that's already been discussed, haven't read everything)

Member

acdlite commented Dec 7, 2017

maybe you expose that information in another special way?

Right, this is what I meant

Published a polyfill package for the proposed API: https://github.com/thejameskyle/create-react-context

Happy to transfer this repo/npm package to the React team and/or add collaborators.

milesj commented Dec 8, 2017

@thejameskyle Great start but this suffers from the problem of not re-rendering if shouldComponentUpdate returns false at some point in the tree. You'll need to use a broadcast channel or an event emitter.

jsg2021 commented Dec 8, 2017

Can you chain providers? I have a case where we used the context to build a breadcrumb of sorts... Where at route points we have providers that also consume... and they append and forward the context down.

What about multiple values per provider?

@milesj Yeah, I just added an event emitter

pluma commented Dec 8, 2017

I really like the idea of keeping the consumer (or "injector" if you want to phrase it in terms of dependency injection) out of JSX. There are already too many nested components when using context-based libraries, having yet another component would only make things worse.

There's really no reason "consume" needs to be used in a render method either.

+Providers and consumers come in pairs—for each provider, there is a
+corresponding consumer.
+
+A provider-consumer pair is created using `React.createContext()`:
@mweststrate

mweststrate Dec 8, 2017

I'm not entirely sure, but I would be tempted to call this createContextType, as actual context instances (in the terminology of the current api) are created per Provider instance. It makes it more clear that the Context(type) could be created once, and after that reused by many different Provider instances.

@gaearon

gaearon Dec 8, 2017

Owner

Not sure I'm following. What are context instances? (I couldn't find a match for this term) Do you mean this.context?

@mweststrate

mweststrate Dec 8, 2017

Yes exactly, so the role value plays in this proposal, is in current React played by this.context. So my first thought when reading was that createContext creates a context in terms of the current api. But instead it creates a pair of components that are not context themselves, but provides the infrastructure to distribute "context"s, now called "values" .

Without any prior knowledge the name is probably ok, but since context is an already existing mechanism (not just a concept, but also a thing) attaching such a different meaning might be little confusing? Not sure about it, so it's just a little note ;-). The example makes it quickly clear

mweststrate commented Dec 8, 2017

Great proposal! Imho, Elegant, simple and good typing support. _o_

blainekasten commented Dec 8, 2017

As I looked at this, it felt like render calls would get muddied-up if a component needed to consume multiple contexts.

I created a composing component to simplify the use of nested render props.
https://www.npmjs.com/package/react-context-composer

reference

Member

acdlite commented Dec 8, 2017

A few downsides to the Context.consume(value => <Child />) proposal in #2 (comment):

  • It looks like an imperative API that you could call from outside React. But it only works because it's rendered as a React child. (In other words, it looks eager, but it's actually lazy.)
  • If we want this thing to accept additional options (like allowDetached), we have to add additional arguments, or an options argument. Now it's like we're reinventing React.createElement.

On the other hand:

  • createPortal (also the experimental createCall / createReturn) is another API that already works like this. We should aim for consistency, one way or another.
  • We could warn if they are called outside render, with an helpful message explaining how they should be used.

Those downsides don't seem too problematic. I don't think most people would confuse trying to call Context.consume from outside React. Perhaps people new to React, but even so, that can be offset by good documentation and helpful warnings like you said, as well as community tutorials.

The benefit of consistency with other existing and experimental APIs definitely outweighs the downsides, IMO. And I don't think createContext being very similar to createElement would be a bad thing necessarily.

+ return (
+ // Pass the current context value to the Provider's `value` prop.
+ // Changes are detected using strict comparison (Object.is)
+ <ThemeContext.Provider value={this.state.theme}>
@camwest

camwest Dec 8, 2017

Can the value be a React element? Can I pass things up and down?

const DownContext: Context<any> = React.createContext('down');
const UpContext: Context<any> = React.createContext('up');

class Foo extends React.Component {
  state = { down: null }

  handleUp = (element) => {
    this.setState({ down: element });
  }

  render() {
    return (
      <UpContext.Provider value={this.handleUp}>
       <DownContext.Provider value={this.state.down}>
         {this.props.children}
       </DownContext.Provider>
      </UpContext.Provider>     
    )
  }
}

class Bar extends React.Component {
  state = { visible: false }

  handleHide = () => {
    this.setState({ visible: false });
  }

  handleShow = () => {
    this.setState({ visible: true });
  }

  render() {
    if (state.visible) {
      return (
        <UpContext.Consumer>
          {handle => handle(<div>I am visible <button onClick={this.handleHide}>Hide</button></div>)}
        </UpContext.Consumer>
      )
    } else {
      return (
        <div>
          <button onClick={this.handleShow}>Show</button>
        </div>
      );
    }
  }
}

class Baz extends React.Component {
  render() {
    return (
      <DownContext.Consumer>
      {element => element}
      </DownContext.Consumer>
    );
  }
}
Owner

gaearon commented Dec 8, 2017

createPortal (also the experimental createCall / createReturn) is another API that already works like this. We should aim for consistency, one way or another.

I'd note that createPortal was made non-experimental at the last minute (since we realized it's more important as a migration path than it seemed). When I wrote the original unstable_createPortal I didn't give much thought to usability since I didn't intend it to be the final API. So I wouldn't care about consistency with it too much. (Same for the experimental ones.)

The original plan was to introduce a "proper" API when we figure out how to change portals to support SSR and cross-renderer jumps. At that point we might as well make it <React.Portal>.

Can reading the immediate context value outside render() be achieved? Something like this, similar to Redux store's getState():

// Artificial example.
class ThemeBackgroundCanvas extends React.Component {
  componentDidMount() {
    this._canvas.fillStyle = ( ThemeContext.getValue() === 'dark' ? 'black' : 'white' );
    this._canvas.fillRect(0, 0, this._canvas.width, this._canvas.height);
  }
  render() {
    return <canvas ref={(el) => { this._canvas = el; }} />;
  }
}
Member

acdlite commented Dec 8, 2017

@sompylasar One of the design goals of the new API is to discourage accessing values outside of React's render cycle, to prevent tearing in asynchronous mode.

For your use case, you could pass the context value to a child component (ThemeBackgroundCanvas) and access it as this.props.theme inside your lifecycle.

+ // The Consumer uses a render prop API. Avoids conflicts in the
+ // props namespace.
+ <ThemeContext.Consumer>
+ {theme => (
@probablyup

probablyup Dec 9, 2017

Have you done any experiments of the effect of a large application with this style on garbage collection? I’ve always been cautious of the “render prop” pattern since it makes function garbage... potentially a lot of it if your component tree has many levels and implementations of contextful wrappers.

@acdlite

acdlite Dec 9, 2017

Member

I have not personally, but let's keep comments in this thread focused on context.

@probablyup

probablyup Dec 9, 2017

If introducing a new context API causes memory performance issues, that’s worth discussing.

@acdlite

acdlite Dec 9, 2017

Member

Lol sorry I read your comment quickly earlier and thought I was about online styles.

Render prop allocations aren’t a huge deal. See this Twitter thread: https://twitter.com/sophiebits/status/938075351414063104

@sompylasar

sompylasar Dec 9, 2017

Let's focus on the context of online styles, lol sorry @acdlite 😂 this thread is getting crazy.

@acdlite

acdlite Dec 9, 2017

Member

That's what I get for commenting on GitHub threads when I'm half in the bag. Holiday parties, amirite.

wmertens commented Dec 9, 2017

I read the document but can't find much about why the force-deep-update approach cannot be used.

The idea being that whenever a context object changes, every element in the tree is walked to see if it has that object in its contextTypes, and will be updated if so, completely bypassing shouldComponentUpdate.

(This only needs do be done for children whose ancestor returned false on shouldComponentUpdate)

Would this not be equally fast as the proposed API? IMHO this is not even a breaking change, more like a bugfix.

Owner

gaearon commented Dec 9, 2017

Not sure what you mean. This is pretty much what the implementation will be doing. But you can't do it with the existing API because there is no way to know when the context has changed and when it hasn't. We could've added setContext to get around this, but there are other problems with that (as mentioned in the proposal).

+```
+
+To update the context value, the parent re-renders and passes a different value.
+Changes to context are detected using `Object.is` comparison. (Referred to as
@Andarist

Andarist Dec 9, 2017

I have literally no idea why you'd like to use Object.is instead of ===. I understand the difference between both of them, but considering a gain it gives (+0, -0, NaN comparisons) in edge case situations it should b outweighed (by a multitude) by the downside of this - forcing people to polyfill Object.is in non es6+ environments.

@gaearon

gaearon Dec 9, 2017

Owner

I don’t think we’re talking about literally using it and requiring anyone to polyfill it :-) We’re talking about using its semantics. Which is something like 5 lines to implement.

@Andarist

Andarist Dec 9, 2017

Oh, then it's fine - although I don't still see much gain in using it over === 😉

@gaearon

gaearon Dec 9, 2017

Owner

It’s easy to accidentally get a NaN in JS. Having it re-render all consumers on every single render accidentally because of it seems non-ideal.

@acdlite

acdlite Dec 9, 2017

Member

OTOH, 5 lines of code isn't nothing when you're scanning 50,000 components (which is the microbenchmark I ran to test this). Should weigh that against likelihood that an Object.is edge case will cause an unnecessary re-render.

@dantman

dantman Dec 9, 2017

https://jsperf.com/equality-performance-test
screen shot 2017-12-09 at 12 51 48 pm

The performance difference is more than I thought, though still probably not very relevant in the grand scheme. I'd bet that the context/shouldupdate pipeline do a bunch of more complex things than equality tests that drown out the performance difference and make it nonexistent.

The hybrid method is actually slower than just a straight Object.is.

@ljharb

ljharb Dec 9, 2017

Those differences seem negligible to me - 5 million versus 24 million renders a second? I'd be surprised if even the Facebook news feed had half a million renders in a single second.

@acdlite

acdlite Dec 9, 2017

Member

I’m pretty comfortable with the restriction that people shouldn’t be relying on Object.is semantics and warning them in DEV so they don’t ship it. We can still make it correct if they do, but if they ignore the warning, the extra (unobservable) perf hit is on them. Though even still it won’t cause any unnecessary renders if we check Object.is at the consumer node.

@acdlite

acdlite Dec 9, 2017

Member

To be extra clear, this hot path we’re discussing only marks a node so that it is visited again later. It doesn’t affect whether the render method is actually called. We still do a local comparison at the render site.

In sync mode, this point is irrelevant because all the work happens in one continuous batch. But in async mode we can time slice, so it’s all about fitting the scanning pass comfortably within a single frame.

@jedwards1211

jedwards1211 Dec 10, 2017

@dantman hopefully it will be about as fast as === on VMs that natively implement Object.is at least.

Member

acdlite commented Dec 9, 2017

@wmertens #2 (comment)

As Dan said, the API will have similar semantics as forceDeepUpdate (without the "force" part). We could do this with the existing API by doing a shallow comparison of the result of getChildContext to detect changes. But current API isn't very ergonomic, relies on propTypes and requires a whole new instance variable. We want an API that's easy to use, guides people to the best practices, and is as fast as possible by default.

@agrcrobles agrcrobles referenced this pull request in gnoff/react-tunnel Dec 9, 2017

Open

prop-types warning #7

jedwards1211 commented Dec 10, 2017

@mjackson

One use case is passing around location objects from the current history API, which is mutable.

I'm kind of surprised by that, I would have thought that history creates a new location object every time the location changes. There's no particular reason the location object has to be mutable, is there?

Obviously it's a bit more efficient to mutate the location object. But it seems risky; had I ever thought of keeping track of past location for whatever reason, I would have been surprised to discover that I would have to copy the location object.

jmeas commented Dec 10, 2017

@jedwards1211 , I'm sure the answer is interesting, but we should probably keep this particular issue on the subject of the new context API. The history issue tracker is a better place to have that discussion.

+# How we teach this
+
+This would be our first API that uses render props. They are an increasingly
+popular pattern in third-party React libraries. However, there could be learning
@vsiao

vsiao Dec 11, 2017

Render callback props are dangerous when combined with PureComponent. I worry about this API encouraging their use. Given that, I would prefer the HOC-first approach.

@wmertens

wmertens Dec 11, 2017

I agree. I would not call them "dangerous" though.

If you want to use the given context in lifecycle functions, you need to create a HOC anyway, and with render props you can choose to do that.

Maybe in this case render props are indeed the best approach, to avoid namespace clashing, and to clear the context object from the element eventually.

Added bonuses are that the API is very visible and completely separate from the current API.

@gaearon

gaearon Dec 11, 2017

Owner

They’re not combined with PureComponent here though. They are given to a special React built-in type :-) So there is no such issue here as far as I can see.

@vsiao

vsiao Dec 11, 2017

Sorry, to clarify: I don't have a problem with this proposal in and of itself. It's more about external effects and how the choice to provide an official API with render props encourages other component authors to do the same, potentially exposing themselves to bugs if they use PureComponent.

jsg2021 commented Dec 11, 2017

@acdlite @gaearon How would one chain / replace context mid-graph with this approach?

See my previous question: #2 (comment)

Basicly, we took advantage of a consumer also being a provider mid-graph to add/replace context values to the leave nodes. (at router nodes) The mid-way consumer/provider would consume the incoming 'breadcrumb' and create a new breadcrumb and pass it down through context.

Member

acdlite commented Dec 11, 2017

@jsg2021

type Breadcrumbs = Array<string>;
const BreadcrumbContext = React.createContext([]);

// The context value is an array. Each Breadcrumb adds a new value to the end.
// Could use a custom transformation passed via props instead.
function Breadcrumb(props) {
  return (
    <BreadcrumbContext.Consumer>
      {breadcrumbs => (
        <BreadcrumbContext.Provider value={[...breadcrumbs, props.breadcrumb]}>
          {props.children}
        </BreadcrumbContext.Provider>
      )}
    </BreadcrumbContext.Consumer>
  );
}
+ return (
+ // The Consumer uses a render prop API. Avoids conflicts in the
+ // props namespace.
+ <ThemeContext.Consumer>
@j-f1

j-f1 Dec 12, 2017

What about a HOC alternative here?

// extend the current Context type:
interface Context<T> {
  consume<K extends string>(prop: K): <U>(component: React.ComponentType<U & { [name in K]: T }>) =>  React.ComponentType<U>
  // simpler:
  consume(prop: string): <U>(component: React.ComponentType<U>) => React.ComponentType<U>
}

// usage:

var theme: Context<'light' | 'dark'> = React.createContext('light')
class _ThemedButton extends React.Component<{ theme: 'light' | 'dark', text: string }, {}> {
  render() {
    return <button style={{ background: this.props.theme }}>
      {this.props.text}
    </button>
  }
}

export const ThemedButton = theme.consume('theme')(_ThemedButton)
@gaearon

gaearon Dec 12, 2017

Owner

The design rationale explains that one of the problems this was meant to solve is the pollution of props namespace. HOCs have this problem. You can always create a HOC on top of this if you'd like though.

@vsiao

vsiao Dec 12, 2017

Not if the HOC takes a mapContextToProps or similar, right?

josephsavona commented Dec 12, 2017

Replacing subscription-based patterns in favor of context's built-in change propgation may require a larger effort. However, as a first step, developers can migrate to the new API without abandoning subscriptions.

I'm a bit concerned that the subscription use-case isn't a more central part of this proposal: a forced, deep update of the tree on every context change is equivalent to keeping application state as a single immutable data structure and passing that entire structure as a prop everywhere - any tiny change to the global state will cause a global re-render. The suggested approach (from comments) seems to be to scope context more narrowly:

Having a single ReduxContext isn't ideal. It should be fine, probably even faster than React Redux today. But for maximum performance, the best practice is going to be scoped providers

How would this be (more) efficient? Some applications can have (many) dozens of Flux stores/Redux reducers, for example when using a "best practice" of normalizing data into stores/reducers by their type (e.g. a user reducer, post reducer, etc). A one context per store/reducer approach would lead to an explosion of contexts and necessitate multiple consumers at each component (one for each reducer that it needs to access). If all Redux apps go from having ~one HOC per component to N, that will almost certainly not be performance-positive, as well as complicating debugging and configuration.

There is a fundamental tension in achieving efficient updates in React apps: in the general case application state is a cyclic object graph, while the UI is always a tree. Any change in the object graph could theoretically necessitate an update to any part of the tree (in the worst case, of the entire tree). This is a fundamental source of complexity and a critical aspect to "get right" for performance, but is left to the developer under the current context API and not addressed by this proposal. This leads to frameworks (such as Relay) that track data dependencies of components and efficiently map changes in the object graph to the minimal set of components that must update in response.

Note that representing an object graph with a persistent data structure is not sufficient to determine what parts of the UI need an update: a more complicated mapping is required and there are inevitable tradeoffs in the granularity of state/UI mapping. More fine-grained (ie object/recrd + field granularity) tracking achieves less wasted re-renders at the cost of greater book-keeping overhead, while coarse-grained (ie whole object/record granularity) tracking potentially re-renders more while using similar book-keeping (aside: the latter appears to be a good balance in practice).

In summary: I'd argue that the real complexity that is currently shifted to user space is about efficient mapping of state changes to UI updates, not the context API. It isn't clear how much value just changing the context API alone provides, and I'm especially concerned about the advice (more fine-grained context objects) as being counter-productive to the eventual goal of reducing subscriptions.

what if we want to consume from two different contexts?

AContext.consume(
  BContext.consume(
    
  )
)

I suspect this adds some complexity

Member

acdlite commented Dec 12, 2017

Here are the API variants proposed in this thread. I've implemented the same set of components in each to illustrate the trade-offs:

JSX render prop using children.

function MultiConsumer() {
  return (
    <FooContext.Consumer>
      {foo => (
        <BarContext.Consumer>
          {bar => <Child foo={foo} bar={bar} />}
        </BarContext.Consumer>
      )}
    </FooContext.Consumer>
  );
}

function Breadcrumb(props) {
  return (
    <BreadcrumbContext.Consumer>
      {breadcrumbs => {
        <BreadcrumbContext.Provider value={[...breadcrumbs, props.breadcrumb]}>
          {props.children}
        </BreadcrumbContext.Provider>;
      }}
    </BreadcrumbContext.Consumer>
  );
}

JSX render prop using render:

function MultiConsumer() {
  return (
    <FooContext.Consumer
      render={foo => (
        <BarContext.Consumer render={bar => <Child foo={foo} bar={bar} />} />
      )}
    />
  );
}

function Breadcrumb(props) {
  return (
    <BreadcrumbContext.Consumer
      render={breadcrumbs => (
        <BreadcrumbContext.Provider value={[...breadcrumbs, props.breadcrumb]}>
          {props.children}
        </BreadcrumbContext.Provider>
      )}
    />
  );
}

Non-JSX render prop:

function MultiConsumer() {
  return FooContext.consume(foo =>
    BarContext.consume(bar => <Child foo={foo} bar={bar} />),
  );
}

function Breadcrumb(props) {
  return BreadcrumbContext.consume(breadcrumbs =>
    BreadcrumbContext.provide(
      [...breadcrumbs, props.breadcrumb],
      breadcrumbs => props.children,
    ),
  );
}
Member

acdlite commented Dec 12, 2017

@josephsavona Let's continue our offline discussion, since you raise some valid points. For anyone following alone, I want to clarify one point:

forced, deep update of the tree on every context change

The proposal is for deep updates, but not for forced deep updates. Intermediate components would not re-render.

It's true that if you have a single context type (like ReduxContext or a RelayContext) then you may need to do some extra work to ensure unchanged parts of the tree don't re-render for every value. PureComponent / shouldComponentUpdate should have you covered, but the argument that this is less ergonomic than manual, fine-grained subscriptions is a good one that we should keep having.

Member

acdlite commented Dec 12, 2017

Another thing I'll try to address in future updates to this proposal are how to integrate with systems (like Relay) that aren't one big immutable tree. We're still figuring this out. Probably some sort of transaction system that can coordinate with React's commit phase.

jsg2021 commented Dec 12, 2017

Crazy idea... when decorators finalize, maybe we could use a context connection decorator :) and we could assign that context value to a propName...

@FooContext.consumeAs('foo')
class extends React.Component {
  static propTypes = {
    foo: PropTypes.object //passed as prop from connected HOC/decorator
  }
  ...
}

I think this would be a nice API for consuming multiple contexts:

const ThemeContext: Context<Theme> = React.createContext('light')
const ReduxContext: Context<State> = createReduxContext(store)

connectContext({
  theme: ThemeContext,
  state: ReduxContext,
})(
  ({theme, state}) => (
    ...
  )
)

Or

@connectContext({
  theme: ThemeContext,
  state: ReduxContext,
})
class MyComponent extends React.Component {
  static propTypes: {
    theme: PropTypes.string,
    state: PropTypes.object,
  }
}
Member

acdlite commented Dec 12, 2017

Neither of those are likely to be the official React API because/however they can be implemented in user space on top of the render prop proposal.

jedwards1211 commented Dec 12, 2017

@acdlite that's true, but wouldn't any implementation of connecting to N contexts on top of the official API you've proposed have to nest N connector components one inside of another? As long as the official API avoids an N+1 nesting problem in some way or another, then I'm happy with it.

jsg2021 commented Dec 12, 2017

@acdlite 😄 yeah, I know... decorators aren't final yet. That's why I said 'crazy idea' ;)

Owner

gaearon commented Dec 12, 2017

As already mentioned in the thread a few times, we are not going to consider APIs that inject props into components (which any HOC-like API does). The proposal explains why it’s a problem (polluting the props namespace), and why we went for another approach instead.

You can still implement HOCs/decorators on top of it. Please let’s avoid further comments suggesting this, as we already had this argument a few times, and are now just going in circles.

Owner

gaearon commented Dec 12, 2017

wouldn't any implementation of connecting to N contexts on top of the official API you've proposed have to nest N connector components one inside of another?

Yes, it would (and that would work fine). It is intentional so that comparisons stay fast when React finds the consumer components in the tree.

If you anticipate that you’ll often want to consume the same multiple contexts, you can abstract them away behind a single component. In fact if you scroll the thread up, you’ll find that somebody even wrote a library to do that 🙂 #2 (comment)

Or you can unify several values that usually change together into a single value.

jsg2021 commented Dec 12, 2017

@gaearon I wasn't really proposing official api, I was thinking out loud to an idea of a way to make the 'nesting' complaints a little less visible...and allow to you to pick the propName... sorry for the noise.

+
+# Alternatives
+
+## setContext
@grncdr

grncdr Dec 12, 2017

Would it be possible to expand the arguments against setContext?

When writing a stateful component, the state in question is not conceptually immutable, but the framework asks (needs) to mediate the mutations: use this.setState instead of modifying this.state directly so that React can, pardon the pun, react to the change.

This proposal delivers a similar (non-broken 🎉!) semantic for context, but in a new (and apparently not without controversy) API style. The quote "this API would only be valuable when combined with mutation" could equally be applied to the value prop on SomeContext.Provider, the value is allowed/expected to change, but React needs to be informed.

Maybe getting acceptable performance out of setContext is way too difficult, or the implementation would add more complexity than is reasonable, or other reasons I have no idea about, which is why it would be good to have more depth in this section. As much as I personally like the new API style, what's written doesn't seem like an especially strong justification for choosing it over setContext.

(Also, apologies if this conversation is a repeat, I am not aware of all the design discussions that may have preceded this RFC)

@grncdr

grncdr Dec 12, 2017

Posted by @gaearon while I was writing this:

It is intentional so that comparisons stay fast when React finds the consumer components in the tree.

This was about nesting consumers, but possibly is also related to why setContext is not a good choice?

@acdlite

acdlite Dec 12, 2017

Member

The quote "this API would only be valuable when combined with mutation" could equally be applied to the value prop on SomeContext.Provider, the value is allowed/expected to change, but React needs to be informed.

React is "informed" by comparing the old value to the new value (!==). Which means this won't work:

// Render method
render() {
  // Provider value is read from component state
  return <Provider value={this.state.value}>{this.props.children}</Provider>
}

// Update value by mutating
value.foo = 'bar';

// Will re-render component, but Provider won't trigger context change because
// previousState.value === currentState.value
this.setState({value});

(This example is a bit contrived but it's a pattern people end up relying on without realizing it. The call to setState could be replaced with a re-render due to new props from the parent.)

Note that this is unlike setState (and presumably a hypothetical setContext API), which triggers a re-render regardless of whether the new state values are referentially equal to the previous ones (unless you use shouldComponentUpdate). That's what I mean when I say setContext is only valuable when combined with mutation: if you use immutable values, referential comparison is sufficient to trigger a change, so an extra API isn't necessary.

@grncdr

grncdr Dec 12, 2017

I see your point, and your previous response to @wmertens also provides a bit more insight. I was imagining setContext acting like forceDeepUpdate, which doesn't really add anything over the shallow compare of getChildContext you mentioned there.

Maybe it's not necessary but I feel like it would improve the RFC to add a bit more of these responses/explanations to this section. Thanks for taking the time to explain (again 😉)

+
+* Not ergonomic. Given how common the context use case is, it shouldn't be so
+ difficult to implement properly.
+* Start-up cost. Setting up subscriptions for every consumer is costly,
@nat-n

nat-n Dec 13, 2017

If I'm reading this correctly, this point is the most crucial criticism of the existing mechanism and practices (since everything else is somewhat mitigable), and as such critical to justifying the proposed mechanism for ensuring context updates are propagated independently of regular state/prop based updates.

However I also find it somewhat surprising. I've usually seen (un)subscriptions implemented as a simple add/remove of a callback from a collection, which I would think (haven't not really analysed any benchmarking data) to be pretty cheap.

I'm deeply curious as to what data or intuition you could provide to support this, and justify the tradeoff.

Member

taion commented Dec 17, 2017

Will there be a way to keep something like <StaticContainer> working with this setup?

I'm aware that it doesn't 100% work right now, but it is convenient to fully freeze a subtree in cases where e.g. it's animating out.

dantman commented Dec 17, 2017

@taion StaticContainer doesn't really work at all with context currently. Context doesn't update properly, so any provider/consumer pair that uses updating context uses a subscription model where the provider passes down some sort of observer, event system, or signal that lets the consumer listen for new values directly. This skips over the StaticContainer entirely.

Member

taion commented Dec 18, 2017

@dantman Yes – it's a bit unfortunate and I'm aware of this. In some cases it works right now and in some cases it doesn't. It'd be nice to have a more reliable way to handle these kinds of things.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment