React: Is there a way to access new context api within ComponentDidMount?

Created on 18 Mar 2018  Ā·  41Comments  Ā·  Source: facebook/react

We are building a react mapbox gl module and we use clone and inject props today.

We were looking into using the 16.2.0 context api but I saw that it will have a new one on 16.3.0 but I canā€™t seem to find a way to read context details
On componentDidMount lifecycle (which makes sense for me to use on the map implementation).

Is there a way around this ?

Question

Most helpful comment

There should be a easy way to access or call context in life-cycle methods.
Yes it can be solved by wrapping our component in another component! But feels like a workaround more than a solution.

I understand it feels like itā€™s an extra wrapper, but itā€™s what makes the new context fast. If we didnā€™t have explicit wrapper nodes in the tree we wouldnā€™t be able to quickly ā€œfindā€ components that need to update.

If you need to access context in lifecycle, take it as a prop.

class Button extends React.Component {
  componentDidMount() {
    alert(this.props.theme);
  }

  render() {
    const { theme, children } = this.props;
    return (
      <button className={theme ? 'dark' : 'light'}>
        {children}
      </button>
    );
  }
}

export default React.forwardRef((props, ref) => (
  <ThemeContext.Consumer>
    {theme => <Button {...props} theme={theme} ref={ref} />}
  </ThemeContext.Consumer>
));

This is almost the same amount of lines as a contextTypes definition.

All 41 comments

Adding a example that I'm trying to have this working: https://codesandbox.io/s/l20yn296w7

EDIT: Following the guidelines from https://github.com/reactjs/rfcs/blob/master/text/0002-new-version-of-context.md#class-based-api

This is how it can be achieved with new api.

class BaseMapElement extends React.Component {
  componentDidMount() {
    console.log(this.props.context);
  }

  render() {
    return null;
  }
}

const MapElement = () => (
  <Context.Consumer>
    {context =>
      <BaseMapElement context={context} />
    }
  </Context.Consumer>
)

So the only way to access through componentDidMount is to redirect into Props?

Edit: Changed to componentDidMount

The higher order component that redirecs to props is a good pattern,but for cases where don't want the extra component usually wat I do is store the context value on the component instance like: this.contextValue = value then access it in the life cycle hooks. It's a bit ugly but that's ok generally I think since your opting out of the nicer, hoc, pattern as an optimization

I'm facing the same dilemma.
There should be a easy way to access or call context in life-cycle methods.
We might need to initialize stuff, check and fetch data or even clean-up on unmount.
People are doing it now with the current supposedly broken context.
Yes it can be solved by wrapping our component in another component! But feels like a workaround more than a solution.

store the context value on the component instance like: this.contextValue = value then access it in the life cycle hooks

Iā€™m pretty sure this is unsafe in async mode. Please donā€™t do this. cc @acdlite

Yes it can be solved by wrapping our component in another component! But feels like a workaround more than a solution.

I agree with that. Would be nice to natively access through componentDidMount and componentWillUnmount to be able to initialize/cleanup things,

In general, using instance vars to be clever and cheat the normal data flow is going to cause problems in async. Just donā€™t. Itā€™s a bit confusing today, because instance vars are the only way to do certain things, like timers. Before we release async weā€™ll publish clearer recommendations ā€” and one day weā€™ll have a new component API that doesnā€™t rely so much on instances.

tl;dr: Use a props indirection. And donā€™t worry too much about the extra component.

Iā€™m pretty sure this is unsafe in async mode. Please donā€™t do this.

How would this be unsafe (and in what way)? It's faily unclear in all this talk about async mode, what "unsafe" means. It's starting to feel like a boogyman who behavior is irrational and unpredictable, which doesn't fill folks with a lot of assurance in a system that is generally liked for it's straightforward, easily understandable, data flow model. I feels like components are back in pre 0.13 land where they are magic objects again.

It's also easy to say "Just add another component", but that's often onerous, and introduces it's own category of bugs and challenges. I't starts to feel like folks have to invent abstractions over a react API in order to be "safe".

sorry ^ if the above sounds annoyed/angry, i didn't intend that tone! on a phhhhooone

It did sound angry haha but I get your point and I share your questions about how/why it will be unsafe

It's starting to feel like a boogyman who behavior is irrational and unpredictable

Iā€™m sorry, but weā€™ve been working on a guidance with specific suggestions for over a month now. Please give us time to collect and publish them as a blog post. It is also difficult to discuss without actual alphas to play with, which is also something weā€™ve been working hard on.

So we have to either not say anything at all, or warn in advance about things that wonā€™t work well. We err on the side of warning but I can see how it can look like weā€™re making it more difficult for you. Iā€™m sure that once the code is out and you can play with it, youā€™ll see what we mean, and itā€™ll make more sense.

How would this be unsafe (and in what way)?

Did you get a chance to watch my talk? This will be a bit hard to explain if you havenā€™t seen the second part of it, because it wouldnā€™t be clear why we are doing this. So Iā€™m asking you to watch it. I hope that my talk will convince you weā€™re aiming to solve a broad class of problems that plagued React since the beginning, and that these features are worth revisiting some assumptions we might have gotten used to.

Assuming youā€™ve seen the talk, hereā€™s a more specific explanation about this particular case. In order to be able to ā€œsuspendā€ rendering like I showed in the demo, React needs to be able to call render() at any point in time, potentially with different props. For example if may set this.props and this.state to props from a new screen (thatā€™s being loaded), call render, but then set it to old this.props and this.state when rendering in response to an interaction on the current version of the tree (e.g. if I press on something while the new screen is loading).

In async, the rule of thumb is: only lifecycles like componentDidMount, componentDidUpdate, and componentWillUnmount and ref callbacks execute at well-defined points in time with props and state that correspond to whatā€™s on the screen. Luckily we only have a few other lifecycles that donā€™t neatly fit into this picture, and we are introducing better alternatives for them (getDerivedPropsFromState, getSnapshotBeforeUpdate). This will be a gradual migration. Again, itā€™s all going to be in the blog post.

Now to the root of this issue. In async mode, React doesnā€™t make guarantees about when and in which order it calls the render method. Which is really something React never guaranteed in the first placeā€”it just happened to be the same order every time. Storing a field in render and then reading it in a lifecycle is not ā€œsafeā€ because you might store a field at the time React called render with different props (such as for a suspended tree that isnā€™t ready yet).

There should be a easy way to access or call context in life-cycle methods.
Yes it can be solved by wrapping our component in another component! But feels like a workaround more than a solution.

I understand it feels like itā€™s an extra wrapper, but itā€™s what makes the new context fast. If we didnā€™t have explicit wrapper nodes in the tree we wouldnā€™t be able to quickly ā€œfindā€ components that need to update.

If you need to access context in lifecycle, take it as a prop.

class Button extends React.Component {
  componentDidMount() {
    alert(this.props.theme);
  }

  render() {
    const { theme, children } = this.props;
    return (
      <button className={theme ? 'dark' : 'light'}>
        {children}
      </button>
    );
  }
}

export default React.forwardRef((props, ref) => (
  <ThemeContext.Consumer>
    {theme => <Button {...props} theme={theme} ref={ref} />}
  </ThemeContext.Consumer>
));

This is almost the same amount of lines as a contextTypes definition.

Yes it can be solved by wrapping our component in another component! But feels like a workaround more than a solution.

Just wanted to second what Dan said that the child function / render prop approach is the _official API_ for the new context- so please use it and let React worry about making sure it's fast. (It will be!)

How would this be unsafe (and in what way)?

The draft Strict Mode docs also touch on some of why mutating the instance (which is just another type of side effect) is dangerous in async mode.

We have a experimental branch following the guidelines proposed here. Can anyone have a look to see if it make sense? https://github.com/commodityvectors/react-mapbox-gl/pull/11

I'm not familiar with this library, so I don't know if people ever make use of refs with its components- but if they did, the withContext mixin on that PR might be a good use case for the new forwardRef API.

That make sense. Thanks for the reference. I'm going to close this issue for now.

Just ran into this issue because I've been trying to see if I can achieve the same thing.

So, from what I can gather from all this, it is not possible in the component which makes use of the Context.Consumer to access the API outside of the child render function. I've come up with something which may work to make this all a little easier though (would really appreciate some feedback if this is not good practice for whatever reason):

const MapElement = (props) => (
  <Context.Consumer>
    {context =>
      <RunOnLifecycle
        runOnMount={() => { /*use context*/ }}
        runOnUnMount={() => { /*use context*/ }}
        runOnUpdate={(prevProps) => { /*use context - compare prevProps with props */ }}
        { ...props }
      />
    }
  </Context.Consumer>
)

And that helper component <RunOnLifecycle/> :

export interface IPropsRunOnLifecycle {
  runOnMount?: () => void;
  runOnUpdate?: (prevProps: object) => void;
  runOnUnMount?: () => void;
  children?: JSX.Element | ReactNode;
  [prop: string]: any;
}

export class RunOnLifecycle extends React.Component<IPropsRunOnLifecycle> {
  componentDidUpdate(prevProps, prevState, snapshot) {
    if (this.props.runOnUpdate != null) {
      this.props.runOnUpdate(prevProps);
    }
  }

  componentDidMount() {
    if (this.props.runOnMount != null) {
      this.props.runOnMount();
    }
  }

  componentWillUnmount() {
    if (this.props.runOnUnMount != null) {
      this.props.runOnUnMount();
    }
  }

  render() { return this.props.children || null; }
}

Wondering if this is going to cause any headaches down the line. Still feels like pretty standard React, if somewhat of a hack.

There are some subtle differences that might make that approach a bad idea. For example, if MapElement was a class component that used refs, the refs would not yet be set when the runOnMount callback was run.

šŸ˜„ I would suggest using a HOC approach for this instead:
https://reactjs.org/docs/context.html#consuming-context-with-a-hoc

The only real downside to using a HOC for this sort of thing has been mitigated by the forwardRef API:
https://reactjs.org/docs/react-api.html#reactforwardref

We took the approach like the react docs and what people said here. It is working well for us so far.

https://github.com/commodityvectors/react-mapbox-gl/blob/master/src/Map.js#L63

There are some subtle differences that might make that approach a bad idea. For example, if MapElement was a class component that used refs, the refs would not yet be set when the runOnMount callback was run.

Thanks for the feedback @bvaughn . At the moment I'm using it purely as a kind of state proxy component which adds / removes things from the UI depending on what is mounted within the context tree. Kind of like portals but within the React component tree. So not actually rendering children or dealing with refs at all.

Will keep in mind if I need to do anything that interacts with refs.

Hey everyone,

I need context data in lifecycle methods. so, I followed the HOC approach after seeing the first few comments and passed the context data as props.
Everything is working as expected. But now, I want to write unit test cases for the components but I am unable to do so.

I would really appreciate it if someone could share how I can write test cases for this scenario.

I am using enzyme, enzyme-adapter-react-16 and jest but having some troubles in doing so.

@AmnArora

At the company I work for, we do the following (note, this might not be the consensus), we export the "naked" component as well and then import it in our tests and manually pass the props.

E.g.

// MyComponent.js
export class MyComponent extends Component { /* ... */ }
export default HOC()(MyComponent)

// MyComponent.spec.js
import { MyComponent } from '...'

// OtherComponents.js
import MyComponent from '...'

Also, adding to this discussion, we encountered the same issue and created this https://www.npmjs.com/package/react-context-consumer-hoc that consumes multiple context.

Everything is working as expected. But now, I want to write unit test cases for the components but I am unable to do so.

@AmnArora Why are you unable to write a unit test? What have you tried? What error are you seeing?

@pgarciacamou Firstly thanks for the quick reply. Well, after not finding anything on the web and posting the query here. I came up with the same solution you mentioned.

The Test cases are working now but this seems like a work around. I'll take a look at https://www.npmjs.com/package/react-context-consumer-hoc and discuss with my team.

Thanks. :100:

@bvaughn The thing is earlier when I was using redux for state management, I shallow copied the Component and used dive() and instance() methods to get the instance of the component.

But none of these methods were available when using context API.

And when I wasn't using any of these methods I was getting the following error: _unknown node with tag 12_

Gotcha.

Both of these sound like issues with the version of Enzyme you're using not properly supporting the new context API. That's unfortunate.

I got massive distaste for redux and unistore (a lot of code pollution / extra moving parts imo) which led me to the following setup which allows our nested components to access a single global state. Everything else that shouldn't be in the global state (ie. text input values, toggle values) are stored within the local state of each nested components.

https://github.com/davalapar/session-context

Hi,

is this problem solved?

I has a scenario where I had 3 components. Creator, Display, Cells.
Cell has my render logic which is used in both Creator and Display.

Currently I need to pass the status if the item is being created or it has to display. I have used Cells in different locations so I need to pass the status as props separately. So I wanted to use context for status but problem is I wanted to modify context only if Display component was mounted. Can we achieve this is React's current versions? (I am using React 16.7)

There have been a couple of comments above showing how to access Context.Consumer values in componentDidMount. Did you try them? Did they not work for some reason?

Note that React 16.6 added a new contextType API for classes which makes reading new context in componentDidMount easy.

https://reactjs.org/docs/context.html#classcontexttype

Yeah, provided you only need to use a single context in your componentā€“ contextType is a convenient option.

Note that React 16.6 added a new contextType API for classes which makes reading new context in componentDidMount easy.

It does not. Even if a single context type is enough, Class.contextType breaks inheritance. The same is true about HOC.

We are pretty explicit in our docs about recommending composition over inheritance for reusing code between components. Beyond that, I don't really understand what you're saying.

The contextType API definitely _does_ make it easy to access context values in componentDidMount (which is what this thread is about).

We are pretty explicit in our docs about recommending composition over inheritance...

That's too broad...

The case I am struggling with right now, is that I have a family of components with a bunch of common functionality needed to support their implementation. Everything is modelled by inheritance perfectly, except... context bits!

Given that it seems I mess with context only in situations like this, context API is pretty frustrating.

...which is what this thread is about...

Yes, this comment is slightly off-topic.

Changing contextType with Context Class breaks redux store :/

Note that React 16.6 added a new contextType API for classes which makes reading new context in componentDidMount easy.

Yeah, provided you only need to use a single context in your componentā€“ contextType is a convenient option.

Is there no way to compose contexts before assigning them to MyCoolComponent.contextType?

My read is that for now, if we want a component that can:

a) Consume multiple contexts
b) Use things from those contexts in methods other than render

This means that we're stuck with the pattern described where the wrapper consumes the context and passes props to the child.

I feel that an ideal situation would be that I could write something like this, and get the best of both worlds (i.e. multiple contexts available throughout the whole class):

MyCoolComponent.contextType =  composeContexts(OneContext, TwoContext, RedContext, BlueContext)

Is there any way to do this?

It works for render method, but not working for any other method....any idea ??

Hey there.
Is there any possibility to use the new context api in a class component's constructor?

We are migrating our projects from v15.x to v16.x, one of the tasks is to use the new context api
In our project, we use css-modules + isomorphic-style-loader , the latter exposes some APIs to inject the component stylesheets into DOM.

In V15, we put those APIs into the old context api, and let each component to get it via something like

    class MyComp extends Component {
        static contextTypes = {
                insertCss: PropTypes.func
           }
         ....
         componentWillMount () {
                // insert a style tag for this component
               this.removeCss = this.context.insertCss(myStyles)
         }
    }

In V15, we can put this in the componentWillMount. This will ensure the component get correct style before render.

In V16, however, the componentWillMount is marked as unsafe and will be deprecated in future.
So our immediate thoughts would be to put our context.insertCss call into component's constructor.

However, seen from the document,

If contextTypes is defined within a component, the following lifecycle methods will receive an additional parameter, the context object:

constructor(props, context)

This usage (with context as a 2nd parameter) will be deprecated too.

I tried the new context api, assigning MyComp.contextType = StyleContext
Yet I find that I get this.context is undefined in constructor.

    class MyComp extends Component {
        static contextType = StyleContext

         constructor (props) {
             super(props)
             console.log(this.context) // undefined
         }

    }

Is there any practical guide on how to use context in constructor?

Any advice?

Hey there.
Is there any possibility to use the new context api in a class component's constructor?

We are migrating our projects from v15.x to v16.x, one of the tasks is to use the new context api
In our project, we use css-modules + isomorphic-style-loader , the latter exposes some APIs to inject the component stylesheets into DOM.

In V15, we put those APIs into the old context api, and let each component to get it via something like

    class MyComp extends Component {
        static contextTypes = {
                insertCss: PropTypes.func
           }
         ....
         componentWillMount () {
                // insert a style tag for this component
               this.removeCss = this.context.insertCss(myStyles)
         }
    }

In V15, we can put this in the componentWillMount. This will ensure the component get correct style before render.

In V16, however, the componentWillMount is marked as unsafe and will be deprecated in future.
So our immediate thoughts would be to put our context.insertCss call into component's constructor.

However, seen from the document,

If contextTypes is defined within a component, the following lifecycle methods will receive an additional parameter, the context object:

constructor(props, context)

This usage (with context as a 2nd parameter) will be deprecated too.

I tried the new context api, assigning MyComp.contextType = StyleContext
Yet I find that I get this.context is undefined in constructor.

    class MyComp extends Component {
        static contextType = StyleContext

         constructor (props) {
             super(props)
             console.log(this.context) // undefined
         }

    }

Is there any practical guide on how to use context in constructor?

Any advice?

You can do like this instead of using contextType

class MyComponent extends React.Component {
   render(){
       const {
         //props including context props
       } = this.props;
       return(<View />);
   }
};

const withContext = () => (
  <MyContext.Consumer>
    { (contextProps) => (<MyComponent {...contextProps}/>)}
  </MyContext.Consumer>
);

export default withContext;

For whoever like me are struggling to use it outside your render function just use the following in your sub-component:

useContext()

For example:

const context = useContext(yourContext)

MainComponent.Subcomponent = () => {
 const context = useContext(context)

  useEffect(()=> {
    console.log(context)
  })
}
Was this page helpful?
0 / 5 - 0 ratings