The land of undocument react.js: The Context

·

Русская версия на Хабре. Перевод не мой, спасибо webMarshal-у за перевод.

If you have any questions, or want to discuss some points — leave a comment here, or tweet me.

If we take a look at React component we can outline some properties.

State

Yes, each react component has state. State is something internal to the component. Only the component it self can read and write into its own state and as the name implies, the state of the component is used to store state (captain obvious here). Not interesting. Let’s move next.

Props

Or shall we say properties. Props is the data that affects the component display and behavior. Props can be optional or mandatory, and they are provided by the parent component. Ideally, when you provide your component with the same props — it renders the same. Not interesting as well. Let’s move on.

Context

Meet context, the reason I’m writing this post. Context is an undocumented feature of react.js and it is similar in a way to props, but the only difference is that props are passed explicitly by parent to child, and they do not propagate down the hierarchy, while context can be requested by children component implicitly.

But why?

Good question. Lets draw!

React state props tree#363x313

We have a Grandparent component that renders Parent A component that renders two child components: Child A and Child B.

Lets say Grandparent component knows something that Child A and Child B wants to know as well, but Parent A doesn’t care about. Lets call this piece of data Xdata. How would Grandparent component give ChildA and ChildB access to Xdata?

Well, using Flux architecture we can store Xdata inside a store and let Grandparent, Child A and Child B subscribe to that store. But what if we want Child A and Child B to be pure dumb components that only renders some markup?

Well, we than can pass Xdata as prop to Child A and Child B. But Grandparent can not pass props to its grand children without going through Parent. And its not that big deal if we have only 3 level nesting, but real applications have a lot more nesting levels where top components acts as containers and leaf components are just markup. Well, we can use mixins that will automatically pass the props down the hierarchy for us, but its not elegant.

Or we can use context. As I said earlier, context allows children component to request some data to arrive from component that is located higher in the hierarchy.

Here is how it looks:

var Grandparent = React.createClass({  
  childContextTypes: {
    name: React.PropTypes.string.isRequired
  },  getChildContext: function() {
    return {name: 'Jim'};
  },
  
  render: function() {
    return <Parent/>;
  }
    
});

var Parent = React.createClass({
 render: function() {
   return <Child/>;
 }
});

var Child = React.createClass({
 contextTypes: {
   name: React.PropTypes.string.isRequired
 }, render: function() {
  return <div>My name is {this.context.name}</div>;
 }
});

React.render(<Grandparent/>, document.body);

And here is a JSBin with the code. Change Jim to Jack and you see how your component re-renders.

WHAT IS HAPPENING?!

Our Grandparent component says two things:

  1. I provide my children with context type named name of type string. This is what happening in childContextTypes declaration.
  2. The value of context type named name is Jim. This is what happening in getChildContext function.

And our child component just says “Hey I expect to have context type named name!” and it gets it. As far as I understand (and I’m far from being expert in react.js internals) when react renders children components it checks which components want to have context and those that want context are provided with context if parent supplies them.

Cool!

Yep, expect when you encounter the following error

Warning: Failed Context Types: Required context `name` was not specified in `Child`. Check the render method of `Parent`.
runner-3.34.3.min.js:1

Warning: owner-based and parent-based contexts differ (values: `undefined` vs `Jim`) for key (name) while mounting Child (see: http://fb.me/react-context-by-parent)

Yes of course I checked the link, its not very helpful.

Here is the code that causes this (JSBin):

var App = React.createClass({
  render: function() {
    return (
      <Grandparent>
        <Parent>
          <Child/>
        </Parent>
      </Grandparent>
    );
  }
});

var Grandparent = React.createClass({  
  childContextTypes: {
    name: React.PropTypes.string.isRequired
  },  getChildContext: function() {
    return {name: 'Jim'};
  },
  
  render: function() {
    return this.props.children;
  }
    
});

var Parent = React.createClass({
  render: function() {
    return this.props.children;
  }
});

var Child = React.createClass({
  contextTypes: {
    name: React.PropTypes.string.isRequired
  },  render: function() {
    return <div>My name is {this.context.name}</div>;
  }
});

React.render(<App/>, document.body);

It doesn’t make sense now, but Ill explain a setup where this is a viable hierarchy in the end of this post.

So what is happening?

It took me a lot of time to understand why this happens. Googling the error only brought me to discussions by people who also wonders why this happens. I looked at other projects like react-router or react-redux that uses context to pass data down the component tree, when eventually I realized what is wrong.

Note: I said I’m not expert in react internals, so everything I’m going to say now is purely what I understood about react internals. Leave a comment if you think I’m wrong (preferably with a link that backups your comment).

Remember I said that each react component have state, props and context? Well, each component have as well so called parent and owner. And if we follow the link from the earlier warning (so yes, it is useful, I lied) we can understand that:

In short, the owner of a component is whomever creates the component, while the parent of a component is whomever would be the containing ancestor in the DOM hierarchy.

It took me some time to understand this statement.

So in my first example, the owner of Child component is Parent, as well as the parent of Child is Parent. While in the second example the owner of Child component is App while the parent is Parent.

Context is something that propagates in a strange way to all the descendant components, but will be assigned only to those components who explicitly asked for context. But context does not propagate from the parent component as you’d expect but from owner! And since the owner of Child is App, React tries to find the “name” attribute in context of App instead of Parent or Grandparent.

Here is the relevant bug report in React. And here is the relevant pull request that should fix this to Parent based context as opposed to Owner based context in React 0.14.

However React 0.14 is not there yet, so here is a fix (JSBin):

var App = React.createClass({
  render: function() {
    return (
      <Grandparent>
        { function() {
          return (<Parent>
            <Child/>
          </Parent>)
        }}
      </Grandparent>
    );
  }
});

var Grandparent = React.createClass({  
  childContextTypes: {
    name: React.PropTypes.string.isRequired
  },  getChildContext: function() {
    return {name: 'Jack'};
  },
  
  render: function() {
    var children = this.props.children;
    children = children();
    return children;
  }
    
});

var Parent = React.createClass({
  render: function() {
    return this.props.children;
  }
});

var Child = React.createClass({
  contextTypes: {
    name: React.PropTypes.string.isRequired
  },  render: function() {
    return <div>My name is {this.context.name}</div>;
  }
});

React.render(<App/>, document.body);

Instead of instancing the Parent and Child components inside App, we return a function. Then inside the Grandparent we call this function hence making Grandparent the owner of Parent and Child components. Context propagates as expected now.

Ok, cool, but… WHY?!

Remember my previous post about localization in react.js? Consider the following hierarchy:

<Application locale="en">
  <Dashboard>
    <SalesWidget>
      <LocalizedMoney currency="USD">3133.7</LocalizedMoney>
    </SalesWidget>
  </Dashboard>
</Application>

This is a static hierarchy, but usually you will have some routing and eventually you will create a situation where the owner and parent of your leaf components — differs.

Application is responsible for loading the locale data and initializing an instance of jquery/globalize. But it doesn’t use it. You don’t localize your top level components. Localization affects usually leaf components like localized text nodes, numbers, money display or time. And I’ve discussed earlier about the 3 possible ways to pass the globalize instance down the tree. Either we store it in a store and let leaf components subscribe to the store, but I think its incorrect. Leaf components should be pure dumb components.

Passing the globalize instance as props can be tedious, imagine ALL your components requires globalize instance. Its the same as making globalize instance a global variable and let those who needs it — use it.

But the most elegant way is using context. Application components says “Hey I have a globalize instance here, anyone who needs it let me know” and any Localized leaf components yells “Me! Me! I want it!”. This is an elegant solution. Leaf components stays pure, they dont depend on stores (yes they depend on context, but they have to, because they need it to render correctly). Globalize instance is not getting passed as props to your entire hierarchy. Every body is happy.


That’s it my friends!

May the force be with you.

Share this:

Published by

Dmitry Kudryavtsev

Dmitry Kudryavtsev

Senior Software Engineer / Tech Entrepreneur

With more than 14 years of professional experience in tech, Dmitry is a generalist software engineer with a strong passion to writing code and writing about code.


Technical Writing for Software Engineers - Book Cover

Recently, I released a new book called Technical Writing for Software Engineers - A Handbook. It’s a short handbook about how to improve your technical writing.

The book contains my experience and mistakes I made, together with examples of different technical documents you will have to write during your career. If you believe it might help you, consider purchasing it to support my work and this blog.

Get it on Gumroad or Leanpub


From Applicant to Employee - Book Cover

Were you affected by the recent lay-offs in tech? Are you looking for a new workplace? Do you want to get into tech?

Consider getting my and my wife’s recent book From Applicant to Employee - Your blueprint for landing a job in tech. It contains our combined knowledge on the interviewing process in small, and big tech companies. Together with tips and tricks on how to prepare for your interview, befriend your recruiter, and find a good match between you and potential employer.

Get it on Gumroad or LeanPub