The world of undocumented React.js. Context

    I offer readers of "Habrahabr" a translation of the article "The land of undocumented react.js: The Context" .

    If we look at the React component, we can see some properties.

    State


    Yes, every React component has a state. This is something inside the component. Only the component itself can read and write in its own state, and as the name implies - state is used to store the state of the component (Hello, Cap). Not interesting, let's continue.

    Props


    Or, say, properties. Props are data that affects the display and behavior of a component. Props can be either optional or mandatory, and they are provided through the parent component. Ideally, if you pass the same Props to your component, it will render the same thing. Not interesting, let's move on.

    Context


    Meet context, the reason I wrote this post. Context is an undocumented feature of React and is similar to props, but the difference is that props are passed exclusively from the parent component to the child component and they do not propagate down the hierarchy, while context can simply be requested in the child element.

    But how?


    Good question, let's draw!



    We have a Grandparent component that renders the Parent A component that renders the Child A and Child B components. Let the Grandparent component know something that Child A and Child B would like to know, but Parent A does not need it. Let's call this piece of data Xdata. How would Grandparent pass Xdata to Child A and Child B?

    Well, using the Flux architecture , we could store Xdata inside the store and let Grandparent, Child A and Child B sign up for this store. But what if we want Child A and Child B to be pure stupid components that just render some markup?

    Well, then we can pass Xdata as props to Child A and Child B. But Grandparent cannot push props to Child A and Child B without passing them to Parent A. And this is not such a big problem if we have 3 levels of nesting, but in a real application, there are much more levels of nesting, where the top components act as containers and the lower ones as normal markup. Well, we can use mixins so that props automatically move down the hierarchy, but this is not an elegant solution.

    Or we can use context. As I said earlier, context allows child components to request some data so that they come from a component located higher in the hierarchy.

    What does it look like:

    var Grandparent = React.createClass({  
      childContextTypes: {
        name: React.PropTypes.string.isRequired
      },
      getChildContext: function() {
        return {name: 'Jim'};
      },
      render: function() {
        return ;
      }
    });
    var Parent = React.createClass({
     render: function() {
       return ;
     }
    });
    var Child = React.createClass({
     contextTypes: {
       name: React.PropTypes.string.isRequired
     },
     render: function() {
      return 
    My name is {this.context.name}
    ; } }); React.render(, document.body);

    And here is the JSBin with the code. Change Jim to Jack and you will see how your component will be rendered.

    What happened


    Our Grandparent component says two things:

    1. I provide my descendants with the string property (context type) name. This is what happens in the declaration of childContextTypes.
    2. The value of the (context type) name property is Jim. This is what happens in the getChildContext method.

    And our child components just say “Hey, I expect a context type name!” and they get it. As far as I understand (I am far from an expert in the interns of React.js), when react renders child components, it checks which components want context and those that want it - they get it if the parent component allows it (supplies context).

    Cool!


    Yes, wait 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, it is not very useful.

    This code is the reason for this JSBin :

    var App = React.createClass({
      render: function() {
        return (
          
        );
      }
    });
    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 
    My name is {this.context.name}
    ; } }); React.render(, document.body);

    This has no meaning now. At the end of the post I will explain how to set up a viable hierarchy.

    It took me a long time to understand what was going on. Attempts to google the problem were given only by discussions of people who also faced this problem. I looked at other projects like react-router or react-redux that use context to push data down the component tree, when in the end I realized what the error was.

    Remember, I said that each component has state, props and context? Each component also has a so-called parent and owner. And if we follow the link from warning (so yes, it is useful, I lied) we can understand that:

    In short, the owner is the one who created the component, when the parent is the component that is higher in the DOM tree.

    It took me a while to understand this statement.

    And so, in my first example, the owner of the Child component is Parent, the parent of the Child component is also Parent. While in the second example, the owner of the Child component is App, when the parent is Parent.

    Context is something that, in a strange way, extends to all descendants, but will only be available from those components that have explicitly requested it. But context is not distributed from the parent, it is distributed from the owner. And still the owner of the Child component is the App, React is trying to find the name property in the App context instead of Parent or Grandparent.

    Here is the correspondingbug report in React. And a pull request , which should fix the context based on the parent in React 0.14.

    However, React 0.14 is not there yet. Fix (JSBin) .

    var App = React.createClass({
      render: function() {
        return (
          
            { function() {
              return ()
            }}
          
        );
      }
    });
    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 
    My name is {this.context.name}
    ; } }); React.render(, document.body);

    Instead of instances of the Parent and Child components inside the App, we return a function. Then inside Grandparent we will call this function, therefore we will make Grandparent the owner of the Parent and Child components. The context spreads as it should.

    OK, but why?


    Remember my previous article on localization in react? The following hierarchy was considered:

    3133.7

    This is a static hierarchy, but usually you have routing and eventually you will create a situation where the owner and parent of your lower component will be different.

    Application is responsible for loading the locale and initializing the jquery / globalize instance, but it does not use them. You do not localize your top-level component. Typically, localization affects the lowermost components, such as text nodes, numbers, money, or time. And I talked earlier about the three possible ways to drag an instance of globalize down the component tree.

    We store globalize in the store and allow the lowest components to subscribe to this store, but I think this is incorrect. The bottom components should be clean and dumb.

    Dragging an instance of globalize as props can be tedious. Imagine ALL of your components require globalize. This is similar to creating a global variable globalize and who needs it - let it use it.

    But the most elegant way is to use context. The Application component says “Hey, I have a globalize instance, if anyone needs to, let me know” and any lower component shouts “Me! I need him!". This is an elegant solution. The lower components remain clean, they do not depend on the store (yes, they depend on the context, but they should, because they need to render correctly). The globalize instance does not go through props through the entire hierarchy. Everyone is happy.

    Also popular now: