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

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

Closed
gaearon opened this issue Aug 7, 2018 · 7 comments
Closed

Comments

@gaearon
Copy link
Collaborator

gaearon commented Aug 7, 2018

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.

@sebmarkbage
Copy link
Collaborator

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

@gaearon
Copy link
Collaborator Author

gaearon commented Aug 7, 2018

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

@sebmarkbage
Copy link
Collaborator

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

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Aug 7, 2018

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.

@gaearon
Copy link
Collaborator Author

gaearon commented Aug 7, 2018

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
Copy link

kball commented Aug 13, 2018

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

@lyleunderwood
Copy link

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>

@bvaughn bvaughn self-assigned this Sep 25, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants