facebook/react

Imperative wrappers can't access current context value in commit phase

gaearon opened this issue · 7 comments

Sometimes we have an imperative wrapper like this:

componentDidMount() {
  renderSomethingImperatively(this.props)
}

componentDidUpdate() {
  renderSomethingImperatively(this.props)
}

render() {
  return null
}

Portals eliminated the need for this for regular DOM jumps. But we still need this for embedding renderers (e.g. react-art does this) and use cases like "Vue inside React".

For cross-renderer embedding, maybe we could extend portals to do that (#13332). There are still imperative use cases for cross-library rendering though.

One thing that becomes annoying is that new context won't propagate down through this imperative boundary. This is because we don't maintain a stack in the commit phase. We're traversing a flat linked list of effects. So we don't actually know what context value is current by the time componentDidMount or componentDidUpdate fires.

For react-art and friends, this means context from a host app is not accessible. This is quite annoying. You could hack around it with something like

<MyConsumer>
  {value =>
    <ReactART.Surface>
      <MyContext.Provider value={value}>
        <Stuff />
      </MyContext.Provider>
    </ReactART.Surface>
  }
</MyConsumer>

But this is neither obvious nor convenient. You have to anticipate all contexts that can get used below.

This seems even less convenient for imperative cases like "Vue inside React".

componentDidMount() {
  renderSomethingImperatively(this.props) // ???
}

componentDidUpdate() {
  renderSomethingImperatively(this.props) // ???
}

render() {
  // <MyConsumer>{value => ???}</MyConsumer>
  return <div />
}

Seems like you could use unstable_read() in getDerivedStateFromProps and that would put it into state so you can use it in lifecycles. So maybe that's sufficient. It still means you need to be explicit about which contexts you want to remember though.

I wonder if we can find a better solution to these use cases.

Any solution will always have to be explicit up-front which contexts you're going to use.

Aside from cross-renderer portals right? If we implement them it'll be in render phase so it'll just work.

You won't be allowed to have side-effectful things in there so will be hard to do the Vue in React thing.

My personal preference is MyClass.contextType = MyContext; and the value gets automatically set on this.context just like the old one but for single values. If you use more than one here, we need to have a hard conversation about your context abuse.

For "Vue in React" use case you don't immediately use more than one, but you probably want the whole "Vue world" below to access a few different contexts from above.

kball commented

@sebmarkbage if it is explicit & not transparent, it would be pretty important to be able to introspect the set of available contexts.

One example usecase for "Vue in React" case is building Gutenberg components with Vue- main reason for context passing is to be able to use Gutenberg builtins that utilize context, so all of the context usage is in 3rd party code.

To work, the Vue in React library needs to be able to either just transparently pass through all contexts, or introspect the contexts set so it can pass them through.

Here's my hacky workaround to this which seems to function but could stand to be generalized:

// @flow
import * as React from 'react';

import { L10nProvider, L10nConsumer } from './l10n-context';
import { SettingsProvider, SettingsConsumer } from './settings-context';

// TODO: react does not currently support seamlessly passing context
// between portals, and therefore renderers, including react-konva
// (canvas renderer). see:
// https://github.com/konvajs/react-konva/issues/188

type Props = {
  children: React.Element<any>,
  barrierRender: (React.Element<any>) => React.Element<any>,
};

const ContextBridge = ({ barrierRender, children }: Props) => (
  <L10nConsumer>{l10nValue => (
    <SettingsConsumer>{settingsValue => (
      barrierRender(
        <L10nProvider value={l10nValue}>
          <SettingsProvider value={settingsValue}>
            {children}
          </SettingsProvider>
        </L10nProvider>
      )
    )}</SettingsConsumer>
  )}</L10nConsumer>
);

export default ContextBridge;

and a usage example (react-konva):

      <ContextBridge
        barrierRender={children => (
          <Stage width={500} height={300} scaleX={1} scaleY={1}>
            {children}
          </Stage>
        )}
      >
        <Layer>
          <L10nConsumer>{({ locale }) => (
            <Text
              text={locale}
            />
          )}</L10nConsumer>
        </Layer>
      </ContextBridge>