Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experimental] API for reading context from within any render phase function #13139

Merged
merged 5 commits into from Jul 20, 2018

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Jul 3, 2018

Context.unstable_read can be called anywhere within the render phase. That includes the render method, getDerivedStateFromProps, constructors, functional components, and context consumer render props.

@pull-bot
Copy link

pull-bot commented Jul 3, 2018

React: size: 🔺+2.4%, gzip: 🔺+1.9%

ReactDOM: size: 🔺+0.7%, gzip: 🔺+0.7%

Details of bundled changes.

Comparing: 5776fa3...47d4520

react

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react.development.js +0.8% +0.8% 57.39 KB 57.84 KB 16.03 KB 16.17 KB UMD_DEV
react.production.min.js 🔺+2.4% 🔺+1.9% 6.86 KB 7.03 KB 2.91 KB 2.96 KB UMD_PROD
react.development.js +0.9% +0.9% 51.58 KB 52.04 KB 14.21 KB 14.34 KB NODE_DEV
react.production.min.js 🔺+2.8% 🔺+2.3% 5.87 KB 6.03 KB 2.51 KB 2.57 KB NODE_PROD
React-dev.js +0.9% +0.9% 49.4 KB 49.85 KB 13.63 KB 13.75 KB FB_WWW_DEV
React-prod.js 🔺+2.4% 🔺+1.9% 13.4 KB 13.72 KB 3.73 KB 3.8 KB FB_WWW_PROD

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.3% +0.3% 639.45 KB 641.14 KB 149.88 KB 150.33 KB UMD_DEV
react-dom.production.min.js 🔺+0.7% 🔺+0.7% 95.43 KB 96.13 KB 30.87 KB 31.09 KB UMD_PROD
react-dom.development.js +0.3% +0.3% 635.59 KB 637.28 KB 148.7 KB 149.15 KB NODE_DEV
react-dom.production.min.js 🔺+0.7% 🔺+0.7% 95.42 KB 96.11 KB 30.39 KB 30.62 KB NODE_PROD
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.0% 11.04 KB 11.04 KB 3.8 KB 3.8 KB UMD_PROD
ReactDOM-dev.js +0.4% +0.4% 641.99 KB 644.6 KB 147.03 KB 147.55 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.8% 🔺+0.8% 276.7 KB 278.78 KB 51.69 KB 52.12 KB FB_WWW_PROD
react-dom.profiling.min.js +0.8% +0.8% 96.51 KB 97.25 KB 30.77 KB 31.03 KB NODE_PROFILING
ReactDOM-profiling.js +0.8% +0.9% 279.7 KB 282.08 KB 52.37 KB 52.84 KB FB_WWW_PROFILING

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.4% +0.5% 425.94 KB 427.67 KB 96.1 KB 96.54 KB UMD_DEV
react-art.production.min.js 🔺+0.9% 🔺+1.1% 82.75 KB 83.53 KB 25.52 KB 25.8 KB UMD_PROD
react-art.development.js +0.5% +0.5% 358.5 KB 360.2 KB 79.03 KB 79.46 KB NODE_DEV
react-art.production.min.js 🔺+1.6% 🔺+1.9% 47.73 KB 48.51 KB 14.9 KB 15.18 KB NODE_PROD
ReactART-dev.js +0.8% +0.7% 347.85 KB 350.46 KB 73.82 KB 74.35 KB FB_WWW_DEV
ReactART-prod.js 🔺+1.4% 🔺+1.7% 148.11 KB 150.19 KB 25.04 KB 25.46 KB FB_WWW_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +0.5% +0.6% 361.77 KB 363.47 KB 79.3 KB 79.74 KB UMD_DEV
react-test-renderer.production.min.js 🔺+1.6% 🔺+2.1% 48.26 KB 49.04 KB 14.83 KB 15.14 KB UMD_PROD
react-test-renderer.development.js +0.5% +0.6% 357.9 KB 359.6 KB 78.33 KB 78.77 KB NODE_DEV
react-test-renderer.production.min.js 🔺+1.6% 🔺+2.2% 47.97 KB 48.75 KB 14.63 KB 14.95 KB NODE_PROD
ReactTestRenderer-dev.js +0.7% +0.7% 362.11 KB 364.73 KB 77.18 KB 77.72 KB FB_WWW_DEV

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +0.5% +0.6% 346.89 KB 348.59 KB 74.92 KB 75.37 KB NODE_DEV
react-reconciler.production.min.js 🔺+1.5% 🔺+2.1% 47.05 KB 47.73 KB 14.12 KB 14.42 KB NODE_PROD
react-reconciler-persistent.development.js +0.5% +0.6% 345.51 KB 347.2 KB 74.36 KB 74.81 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+1.5% 🔺+2.1% 47.06 KB 47.74 KB 14.12 KB 14.42 KB NODE_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +0.5% +0.5% 480.33 KB 482.9 KB 106.03 KB 106.6 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+1.1% 🔺+1.3% 212.57 KB 214.99 KB 36.97 KB 37.47 KB RN_FB_PROD
ReactNativeRenderer-dev.js +0.5% +0.5% 480.07 KB 482.63 KB 105.97 KB 106.54 KB RN_OSS_DEV
ReactNativeRenderer-prod.js 🔺+1.5% 🔺+1.5% 202.02 KB 205.04 KB 35.32 KB 35.85 KB RN_OSS_PROD
ReactFabric-dev.js +0.5% +0.6% 470.54 KB 473.11 KB 103.59 KB 104.16 KB RN_FB_DEV
ReactFabric-prod.js 🔺+1.6% 🔺+1.7% 193.04 KB 196.15 KB 33.75 KB 34.31 KB RN_FB_PROD
ReactFabric-dev.js +0.5% +0.6% 470.58 KB 473.14 KB 103.61 KB 104.18 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+1.6% 🔺+1.7% 193.07 KB 196.19 KB 33.76 KB 34.33 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +1.7% +1.5% 205.16 KB 208.55 KB 36 KB 36.55 KB RN_OSS_PROFILING
ReactFabric-profiling.js +1.7% +1.8% 195.81 KB 199.21 KB 34.36 KB 34.97 KB RN_OSS_PROFILING
ReactNativeRenderer-profiling.js +1.3% +1.4% 215.67 KB 218.47 KB 37.65 KB 38.17 KB RN_FB_PROFILING
ReactFabric-profiling.js +1.7% +1.8% 195.77 KB 199.17 KB 34.34 KB 34.95 KB RN_FB_PROFILING

Generated by 🚫 dangerJS

@sebmarkbage
Copy link
Collaborator

This is a larger file increase than I would've expected. Do we understand why?

@acdlite
Copy link
Collaborator Author

acdlite commented Jul 3, 2018

@philipp-spiess
Copy link
Contributor

Exciting! Do you know if this will break the unstable_renderSubtreeIntoContainer API? When using this API, the new context is not propagated properly (#12493). Of course, there's a reason this API is marked unstable but probably still good to know 😊

@acdlite
Copy link
Collaborator Author

acdlite commented Jul 3, 2018

No, that API is not being supported going forward. Use a portal please :) (Do portals not work for you?)

@@ -135,6 +135,7 @@ import {
commitAttachRef,
commitDetachRef,
} from './ReactFiberCommitWork';
import * as dispatcher from './ReactFiberDispatcher';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid exporting everything. Make it explicit what you want this object to contain. Too easy to accidentally bring in too much. For example, I think the resulting object that rollup produces for this is not what you expect. (Let me know if you see this pattern elsewhere so we can fix it.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured it was ok in this case because I need to use an object for dynamic dispatch. But I agree explicit is better.

@philipp-spiess
Copy link
Contributor

I figured that, but that would still make it a breaking change for libraries like react-redux I suppose (which is still using the legacy context API reduxjs/react-redux#898). Just wanted to bring it up though :)

And yea, portals don't work for reparenting #13044 - But that was risky all along.

@acdlite
Copy link
Collaborator Author

acdlite commented Jul 3, 2018

Thought of a test case: reading context inside PureComponent or some other component that implements shouldComponentUpdate. I believe what should happen is that if there's a pending context change, we skip sCU entirely.

}
}

export function checkForPendingContext(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is way too slow to iterate through these every time we rerender even if we're about to just rerender anyway. We'll need a different approach.

This should already have been known by the context propagation so we just need some way of storing that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the part where we continue the propagation where we left off?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do that part only if any of the contexts matched/changed. The point is to optimize for the case when zero context changed.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A different approach would be to keep track of the currently rendering fiber. Which we already do with current owner, so maybe we can find some overlap there. We're also going to need that for other things anyway.

We could set the current fiber in the beginning of beginWork so that it is properly set for all code branches since essentially anything can call this. So we save some code there.

Then right before render we reset firstContextReader to null on that fiber. It'll be local anyway since it is used by that function.

Then in the context reader function we check if firstContextReader is null. If it is, then reset lastContextReader (that one is still global but you can get rid of firstContextReader) to the new entry. If it is not, then use append it to the lastContextReader.

Note how this minimizes the number of operations we have to do when no context is used.

function getContextChangedBits(context: ReactContext<any>): number {
return isPrimaryRenderer ? context._changedBits : context._changedBits2;
export function prepareToReadContext(): void {
nextFirstReader = nextLastReader = null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You do this in finishReadingContext also. Only it do it once per loop iteration. Either before the loop and in finishReadingContext or in prepareToReadContext. If you keep prepareToReadContext, we should inline all callsites. I don't trust tooling to make this fast.

popProvider(providerFiber: Fiber): void,
getContextCurrentValue(context: ReactContext<any>): any,
getContextChangedBits(context: ReactContext<any>): number,
export type ContextReader<T> = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ContextDependent?

@@ -124,6 +125,9 @@ export type Fiber = {|
// The state used to create the output
memoizedState: any,

// A linked-list of contexts that this fiber depends on
firstContextReader: ContextReader<mixed> | null,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contextDep?

@acdlite
Copy link
Collaborator Author

acdlite commented Jul 5, 2018

@sebmarkbage Updated

changedBits,
renderExpirationTime,
);
if ((changedBits & dependency.observedBits) !== 0) {
Copy link
Collaborator Author

@acdlite acdlite Jul 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebmarkbage As you pointed out, we already did this check while scanning from the provider. But where to store that data? On the update queue, perhaps? I'd rather not put it in the expiration time, if we can avoid it, since that field is already pretty confusing and overloaded.

@acdlite
Copy link
Collaborator Author

acdlite commented Jul 5, 2018

Went with this approach for now:

Track whether any contexts have changed, on the stack. If this flag is false, we can skip checking any context dependencies. Since it's unusual that more than one context updates within the same batch, this should save us a lot of unnecessary checks. (EDIT: It's more about optimizing for the case where no contexts have changed at all, especially high priority updates.)

@gaearon
Copy link
Collaborator

gaearon commented Jul 5, 2018

it's unusual that more than one context updates within the same batch

Is it?

If people don't listen to our recommendations and write <Provider value={inlineObject}> then any update at the very top will likely trigger multiple providers.

Since updates at the very top don't happen that often for most apps, this issue isn't very noticeable, and I'd expect that when popular libraries start using context more, it can become a problem.

Note I haven't read your code so I might have misunderstood your comment.

@acdlite
Copy link
Collaborator Author

acdlite commented Jul 6, 2018

@gaearon I don't think there's much we can do to optimize for that case, anyway. This is more about optimizing for the case where no contexts have changed at all. Especially during a high priority update.

@acdlite
Copy link
Collaborator Author

acdlite commented Jul 17, 2018

Ok, new approach for bailouts after discussing with @sebmarkbage:

Currently, the expirationTime field represents the pending work of both the fiber itself — including new props, state, and context — and of any updates in that fiber's subtree.

The latest commit adds a second field called childExpirationTime. Now expirationTime only represents the pending work of the fiber itself. The subtree's pending work is represented by childExpirationTime.

The biggest advantage is it requires fewer checks to bailout on already finished work. For most types of work, if the expirationTime does not match the render expiration time, we can bailout immediately without any further checks. This won't work for fibers that have shouldComponentUpdate semantics (class components), for which we still need to check for props and state changes explicitly.

@jaredpalmer
Copy link
Contributor

jaredpalmer commented Aug 7, 2018

Is this mainly for theming or as a full alternative to Context.Consumer?

For example, in Formik's <Field> I need to access to some methods off of parent context inside a handleChange function at the moment. Right now, this it's a class property. Is it going to be better to just define that function now in render using Context.read()? I would be all for that. Would love to reduce the noise, extra typings, and ceremony around context usage.

@gaearon gaearon deleted the unifycontext branch August 7, 2018 13:28
@gaearon
Copy link
Collaborator

gaearon commented Aug 7, 2018

What do you mean by theming?

I don't think this will help your use case because unstable_read() only works in render, constructor, and getDerivedStateFromProps. You can't read it in a change handler.

@gaearon
Copy link
Collaborator

gaearon commented Aug 7, 2018

Is it going to be better to just define that function now in render using Context.read()?

Oh, I missed this. Hmm. If you're okay with defining it in render you can already do today with Consumer, can't you?

This method just provides some convenience, and is intended for library use cases. We'll share more later.

@jaredpalmer
Copy link
Contributor

What do you mean by theming?

by theming, i mean a top-level context without a provider, mostly static values i guess.

I don't think this will help your use case because unstable_read() only works in render, constructor, and getDerivedStateFromProps. You can't read it in a change handler.

I could just handleChange in my constructor? right?

class Field extends React.Component {
   constructor() {
     this.handleChange = (e) => {
       const { handleChange, validateOnChange } = FormikContext.read();
       handleChange(e);
       if (!!validateOnChange && !!this.props.validate) {
          // run field-level validations
         this.runFieldValidations(e.target.value);
       }
     }
   }

   render() {
    // pass down this.handleChange eventually
   }
}

.

@gaearon
Copy link
Collaborator

gaearon commented Aug 7, 2018

I could just handleChange in my constructor? right?

Hmm no, your read call is still in handleChange 😉 Just placing it in a constructor doesn't make it execute during the constructor. React doesn't care where you define functions, it's when they're executed that matters.

@jaredpalmer
Copy link
Contributor

Doh! I haven't had my coffee yet this am. 😜Makes sense.

@markerikson
Copy link
Contributor

Any ETA on this being released? I suspect that React-Redux v6 will need to make use of this, and I'm hoping to do some hacking on it next week at React Rally. (Yeah, I know I can do a custom build of React locally, but would also be good to know when it might be out officially.)

@sebmarkbage
Copy link
Collaborator

It is likely that whatever is exposed is a different API than this one.

@markerikson
Copy link
Contributor

What differences would you expect to see from what's here?

@sebmarkbage
Copy link
Collaborator

@markerikson Can you explain a bit more about why you need it? Why isn't the render prop API sufficient?

@markerikson
Copy link
Contributor

markerikson commented Aug 9, 2018

@sebmarkbage : React-Redux's connect() generates a HOC that wraps around the user-provided "plain" component. Up until now, our <Provider> has put the store itself into old context, and every connected component instance subscribes to the store directly.

We plan to switch to new context in v6, and my original proof-of-concept experiment in reduxjs/react-redux#898 puts the store state into context rather than the whole store, with the hope that that is more async-compatible (and possibly even more performant).

While trying to update our 5.x branch to not warn in StrictMode, we've come up with changes that seem to mostly behave, and rely on doing work in getDerivedStateFromProps. I plan on retrying the changes from 898 on top of that as a starting point (per reduxjs/react-redux#980 ).

Meanwhile, the current advice for accessing new context values in lifecycle methods is "create an intermediate component" ( https://reactjs.org/docs/context.html#accessing-context-in-lifecycle-methods ), but that means adding a second level of wrapping, which I'd like to avoid if possible.

Ultimately, what it boils down to is that if we want to keep a single wrapper component instead of two levels of wrapper components, we almost definitely need to read the store state from context in render-phase methods in that one wrapper component, rather than doing all the work in the <Context.Consumer>'s render prop.

(Please feel free to correct me if I'm mistaken in some of my assumptions somewhere.)

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Aug 10, 2018

To manage refs you can use forwardRef to forward any ref to the inner component to avoid wrappers for the HoC itself.

Note that this API doesn't let you access context in life-cycles. So you need some way to stash it if you expect to use it life-cycles which requires you to resort to hacks anyway. The second level of wrapping helps avoid the need to do that since it's accessible from props.

It sounds more like you would want this API: #13336 (comment)

(I will note that avoiding wrappers is in itself an anti-pattern. It's not really all that much slower to have a wrapper than the extra objects required by this API. I understand this is annoying in the devtools but we should just fix that there with blackboxing.)

@bvaughn
Copy link
Contributor

bvaughn commented Aug 10, 2018

I don't think Mark is asking about ref-management. My take on his comment is that he's asking how to avoid creating two wrapper components/fibers for every connected user component, which the latter part of Seb's response targets.

If the remaining concern then is DevTools overhead– we should work together to eliminate that. Maybe this means filtering out types of fibers, maybe it means a more custom solution like this one.

@markerikson
Copy link
Contributor

Yeah, what Brian said.

There's a few reasons for this:

  • Our current implementation uses one wrapper component, and I'd be using that as a starting point for developing v6
  • v5 was a distinct perf improvement over v4, and most of that seems to have been from moving the "should we update?" behavior from inside of the wrapper component's render() method to inside of a memoized selector
  • The amount of potential nesting in the React DevTools, especially when people connect large numbers of components in their application

If we were to go to two wrappers and use of a Context.Consumer, a given portion of the DevTools tree would probably look like:

<App>
  <Connect(MyComponent)>
     <ConnectInner(MyComponent)>
       <ReactReduxContext.Consumer>
         <MyComponent>

That starts to get awfully silly.

I'm certainly open to suggestions on the best way to approach implementing this.

@bvaughn
Copy link
Contributor

bvaughn commented Aug 10, 2018

Sounds like there are two concerns: (1) performance and (2) DevTools UX

Sounds like (1) may not really be a problem (or at least that we should try it first and see).

As for (2), we have an open DevTools issue that is related. If we provided a filtering option for HOCs, for example, that would prune your example tree above to:

<App>
   <ReactReduxContext.Consumer>
     <MyComponent>

We also plan to add the option to filter certain types of fibers (e.g. context) which could further trim it down to:

<App>
   <MyComponent>

Not sure what the exact UI/UX for this will be yet, but it's something I think we can solve fairly easily on the DevTools side.

@markerikson
Copy link
Contributor

Yeah, I've been trying to set up some perf benchmarks so that I can easily test multiple versions of React-Redux against each other, as a precursor to actually sitting down and working on v6. Made some progress - I've got a small Puppeteer harness that can run a specific benchmark and report FPS results (using fps-emitter in the app, which is based on rAF), and also capture Chrome perf traces. The very rough initial results I'm seeing so far indicate that the notional v5.1 PR we had is noticeably slower than v5, although right now all I'm doing is "crank up the number of connected components and see which version chokes the most". The initial proof-of-concept v6 I did a few months ago with use of new context seems competitive with v5, but I suspect the implementation is all wrong async-wise.

I've sure you've got plenty of plans for ReactRally, but maybe I can steal a few minutes of your time while we're there? I'm hoping to hack on v6 Saturday and Sunday, so any advice I can get would be appreciated :)

@bvaughn
Copy link
Contributor

bvaughn commented Aug 10, 2018

We'll see how things go! I'll be there for less than two days, so I might not have a ton of free time, but we can probably chat for a little bit at least.

@pie6k
Copy link

pie6k commented Oct 18, 2020

Just to confirm - it's not avaliable anymore?

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

Successfully merging this pull request may close these issues.

None yet

10 participants