Мир недокументированного React.js. Context

    Предлагаю читателям «Хабрахабра» перевод статьи «The land of undocumented react.js: The Context».

    Если мы взглянем на React компонент то мы можем увидеть некоторые свойства.

    State


    Да, каждый React компонент имеет state. Это что-то внутри компонента. Только сам компонент может читать и писать в свой собственный state и как видно из названия — state используется для хранения состояния компонента (Привет, Кэп). Не интересно, давайте дальше.

    Props


    Или, скажем, properties. Props — это данные, которые оказывают влияние на отображение и поведение компонента. Props могут быть как опциональны так и обязательны и они обеспечиваются через родительский компонент. В идеале, если Вы передаете своему компоненту одинаковые Props — он отрендерит одно и тоже. Не интересно, давайте двигаться дальше.

    Context


    Встречайте context, причину, по которой я написал этот пост. Context — это недокументированная особенность React и похожа на props, но разница в том, что props передается исключительно от родительского компонента к дочернему и они не распространяются вниз по иерархии, в то время как context просто может быть запрошен в дочернем элементе.

    Но как?


    Хороший вопрос, давайте нарисуем!



    У нас есть компонент Grandparent, который рендерит компонент Parent A, который рендерит компоненты Child A и Child B. Пусть компонент Grandparent знает что-то что хотели бы знать Child A и Child B, но Parent A это не нужно. Давайте назовем этот кусок данных Xdata. Как бы Grandparent передал Xdata в Child A и Child B?

    Хорошо, используя архитектуру Flux, мы могли бы хранить Xdata внутри store и позволить Grandparent, Child A и Child B подписаться на этот store. Но что если мы хотим, чтобы Child A и Child B были чистыми глупыми компонентами, которые просто рендерят некоторую разметку?

    Ну, тогда мы можем передать Xdata как props в Child A и Child B. Но Grandparent не может протащить props в Child A и Child B, не передавая их в Parent A. И это не такая уж большая проблема если у нас 3 уровня вложенности, но в реальном приложении гораздо больше уровней вложенности, где верхние компоненты действуют как контейнеры, а самые нижние — как обычная разметка. Хорошо, мы можем использовать mixins, чтобы props автоматически переходили вниз по иерархии, но это не элегантное решение.

    Или мы можем использовать context. Как я говорил ранее, context позволяет дочерним компонентам запрашивать некоторые данные, чтобы они пришли из компонента, расположенного выше по иерархии.

    Как это выглядит:

    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);
    

    А здесь JSBin с кодом. Измените Jim на Jack и Вы увидите как Ваш компонент перерендерится.

    Что произошло?


    Наш Grandparent компонент говорит две вещи:

    1. Я обеспечиваю своих потомков string свойством (context type) name. Это то что происходит в декларировании childContextTypes.
    2. Значение свойства (context type) name — Jim. Это то, что происходить в методе getChildContext.

    И наши дочерние компоненты просто говорят «Эй, я ожидаю context type name!» и они получают это. На сколько я понимаю (я далеко не эксперт во внутренностях React.js), когда react рендерит дочерние компоненты, он проверяет, какие компоненты хотят иметь context и те, что хотят — его получают, если родительский компонент позволяет это (поставляет context).

    Круто!


    Да, ждите, когда столкнетесь со следующей ошибкой:

    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)
    

    Да, конечно, я проверил ссылку, она не очень полезна.

    Этот код — причина этого 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);
    

    Это не имеет смысловой нагрузки сейчас. В конце поста я объясню как настроить жизнеспособную иерархию.

    Мне потребовалось много времени, чтобы понять что происходит. Попытки загуглить проблему выдали только обсуждения людей, кто так же столкнулся с этой проблемой. Я смотрел на другие проекты типа react-router или react-redux, которые используют context для проталкивания данных вниз по дереву компонентов, когда в конце концов я понял в чем ошибка.

    Помните, я говорил, что каждый компонент имеет state, props и context? Так же каждый компонент имеет так называемых родителя (parent) и владельца (owner). И если мы перейдем по ссылке из warning (так да, она полезна, я соврал) мы можем понять, что:

    В кратце, владелец — это тот кто создал компонент, когда родитель — это компонент, который выше в DOM дереве.

    Мне потребовалось время, чтобы понять это заявление.

    И так, в моем первом примере владелец компонента Child — это Parent, родитель компонента Child — это тоже Parent. В то время, как во втором примере владелец компонента Child — это App, когда родитель — это Parent.

    Context — это что-то, что странным образом распространяется на всех потомков, но будет доступен только у тех компонентов, кто явно попросил об этом. Но context не распространяется из родителя, он распространяется из владельца. И по-прежнему владелец компонента Child — это App, React пытается найти свойство name в контексте App вместо Parent или Grandparent.

    Здесь соответствующий bug report в React. И pull request, который должен пофиксить context, основанный на родителе в React 0.14.

    Однако React 0.14 еще не там. Фикс (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);
    

    Вместо экземпляров компонентов Parent и Child внутри App мы возвращаем функцию. Тогда внутри Grandparent мы вызовем эту функцию, следовательно сделаем Grandparent собственником компонентов Parent и Child. Контекст распространяется как надо.

    ОК, но зачем?


    Помните мою предыдущую статью про локализацию в react? Рассматривалась следующая иерархия:

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

    Это статичная иерархия, но обычно у Вас есть роутинг и в конечном счете Вы создадите ситуацию где владелец и родитель Вашего нижнего компонента будут различаться.

    Application отвечает за загрузку locale и инициализацию экземпляра jquery/globalize, но оно не использует их. Вы не локализовываете Ваш компонент верхнего уровня. Обычно локализация оказывает влияние на самые нижние компоненты, такие как текстовые узлы, цифры, деньги или время. И я рассказывал ранее о трех возможных путях протаскивания экземпляра globalize вниз по дереву компонентов.

    Мы храним globalize в store и позволяем самым нижним компонентам подписываться на этот store, но, я думаю, это некорректно. Нижние компоненты должны быть чистыми и глупыми.

    Протаскивание экземпляра globalize как props может быть утомительным. представьте, что ВСЕ Ваши компоненты требуют globalize. Это похоже на создание глобальной переменной globalize и кому надо — пусть пользуется.

    Но самый элегантный путь — это использование контекста. Компонент Application говорит «Эй, у меня экземпляр globalize, если кому надо — дайте знать» и любой нижний компонент кричит «Мне! Мне он нужен!». Это элегантное решение. Нижние компоненты остаются чистыми, они не зависят от store (да, они зависят от контекста, но они должны, потому что им надо отрендериться корректно). Экземпляр globalize не проходит в props через всю иерархию. Все счастливы.
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну, и что?
    Реклама
    Комментарии 28
    • 0
      Интересно, а если данные, положенные в getChildContext не статичные, а подтягиваются из стора. И в случае, если данные изменились, то в дочерних элементах все автоматически обновится или нужно что-то на подобии componentWillReceiveProps?
      • +10
        Но самый элегантный путь — это использование контекста.

        По рукам надо за такое бить. Не документированная фича — это не просто так, что разработчики забыли. Это очень спорная фича, не до конца реализованная и не факт, окажется ли она в 1.0, или будет точно такой же в следующих релизах.
        • 0
          Разработчики заявляли, что эта фича войдет в 1.0 и будет допилена и задокументирована. К сожалению, под рукой нет пруфа.
          • 0
            Context'ы — задокументированная фича, но не полностью, тот же React Router работает как раз на основании контекстов. Но дальше чем роутинг, я бы контекстами не пользовался, иначе получим проблему скоупов от Ангуляра. Проще уж Стор сделать для таких данных
            • 0
              скоупы ангуляра весело позволяют и подталкивают к тому, что бы организовывать коммуникацию соседних компонент через общий скоуп
              • 0
                RR отказывается от контекстов в версии 1.0, на сколько мне известно.
            • +1
              забавное решение, но одна из догм react это реиспользование компонентов. а это позволительно только в ситуации когда дочерние компоненты ничего не знают о родительских (i.e. не имеют зависимостей кроме props). flux придуман не просто так.
              • 0
                Pub/Sub выглядит более гибким, и главное легальным, механизмом обмена данными между уровнями компонентов.
                • +2
                  Бесспорно, но в глупых компонентах, которые в самом низу в иерархии не должно быть обращений к стору, поэтому хотя бы для таких компонентов контекст имеет место быть.
                  • +2
                    PubSub? Это когда дочерний компонент лезет к глобальной переменной за регистрацией?
                    • +1
                      да. только не обязательно глобальной. доступной обоим компонентам, между которыми нужно расшарить данные.
                      • 0
                        Ну а как вы можете снаружи настроить, в какой паб-саб и по какому пабу и сабу компонент будет общаться?

                        Паб-саб это в чистом виде глобальная переменная.
                        • 0
                          вы правы, по сути, это я к словам придираюсь, что с современными возможностями (говорю про browserify и любой другой dep manager) store не обязательно будет в глобальной переменной. всё приложение чаще всего внутри анонимной функции :) но сути вопроса это не меняет, да.
                          • 0
                            Вы сейчас говорите о том, что стор будет переменной, переданной снаружи. Но ведь нельзя снаружи компонента управлять тем, откуда эта переменная будет получена.

                            В ангуляре можно (dependency injection), в Flux нельзя. Flux это плохо и неправильно.

                            Плюс к этому Flux выкидывает в помойку половину того, что есть в реакте, отказываясь от props
                            • +2
                              Вам впору статью писать о плюсах/минусах AngularJS и React/Flux с точки зрения архитектуры приложения. Наконец-то может получиться интересный обзор от того, кто более-менее глубоко работал и с тем и с тем. А то обычно какой-то перекос, когда обзор пишет человек, который с одним инструментом работал долго и глубоко, а второй попытался освоить за недельку.
                              • 0
                                Да, постараюсь =)

                                Мы и с тем, и с тем поработали.
                              • 0
                                немного не понял, причём здесь то, откуда и чем управлять. я всего лишь утверждаю, что с современными инструментами, какой-нибудь PostsStore не будет являться property объекта window, а будет переменной, расшаренной только компонентам, которые этот store require-нули внутри анонимной функции.
                                • 0
                                  компонент сам заказывает, откуда ему взять PostsStore. Ему не передали его сверху, а он сам его нашел. Это и называется глобальная переменная
                    • +1
                      Вы всё правильно написали, это хорошая штука.

                      Флюксы-шмуксы это не решение, а временный запил от непонимания того, как именно жить с реактом в продакшне. Вот одну идею сделали, а остальные её составляющие не продумали.

                      Вроде всё хорошо началось: немутабельные контролы, асинхронный setState, а потом фигак и глобальные переменные на которые надо подписываться. Это, конечно, зло. Основная идея немутабельного и чистого подхода в том, что тот, кто владеет компонентом, может полностью управлять его поведением.

                      Как можно управлять поведением, если компонент сам шастает по глобальным переменным и чего-то там делает, я не понимаю.
                      • 0
                        Так компонент не должен шастать нигде, глупые компоненты управляются владельцем на основе props. PubSub не подходит тут.
                        • 0
                          А как вы разделяете глупые и умные компоненты?
                          • +1
                            В разных папках держу) умные обращаются к стору, глупые получают данные через пропс и контекст.
                            • +1
                              Тоже вариант, но это всё попытки ad-hoc придумать как же жить с реактовским подходом в реальной жизни.
                              • –2
                                Уважаемый, вы либо троллите, либо заблуждаетесь. Само понятие глупый подразумевает незнание. Чтобы получить что-то в контекст, это что-то нужно попросить.
                                Глупый компонент — это компонент имеющий пропсы, редко стейт (на пример кастомный checkbox). Но никак не контекст.
                                • 0
                                  Компонент, который в самом низу иерархии, но зависит от контекста — не является глупым? Данный в него передавать нужно через props или не пытаться иметь внизу иерархии глупый компонент?
                            • 0
                              Подходит. Делаете глупый компонент, зависящий только от props, и умный, в котором делаете PubSub к чему угодно. Не обязательно все-все смешивать в одном компоненте.
                        • 0
                          Теперь я понял что имел ввиду Дэн Абрамов в этой тудушке
                          React.render(
                            // The child must be wrapped in a function
                            // to work around an issue in React 0.13.
                            <Provider store={store}>
                              {() => <App />}
                            </Provider>,
                            rootElement
                          );
                          

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое