Предлагаю читателям «Хабрахабра» перевод статьи «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 через всю иерархию. Все счастливы.

Комментарии (27)


  1. DeLaVega
    08.09.2015 13:09

    Интересно, а если данные, положенные в getChildContext не статичные, а подтягиваются из стора. И в случае, если данные изменились, то в дочерних элементах все автоматически обновится или нужно что-то на подобии componentWillReceiveProps?


  1. hell0w0rd
    08.09.2015 13:31
    +10

    Но самый элегантный путь — это использование контекста.

    По рукам надо за такое бить. Не документированная фича — это не просто так, что разработчики забыли. Это очень спорная фича, не до конца реализованная и не факт, окажется ли она в 1.0, или будет точно такой же в следующих релизах.


    1. WebApelsin
      09.09.2015 16:04

      Разработчики заявляли, что эта фича войдет в 1.0 и будет допилена и задокументирована. К сожалению, под рукой нет пруфа.


    1. Arilas
      09.09.2015 23:07

      Context'ы — задокументированная фича, но не полностью, тот же React Router работает как раз на основании контекстов. Но дальше чем роутинг, я бы контекстами не пользовался, иначе получим проблему скоупов от Ангуляра. Проще уж Стор сделать для таких данных


      1. erlyvideo
        10.09.2015 00:37

        скоупы ангуляра весело позволяют и подталкивают к тому, что бы организовывать коммуникацию соседних компонент через общий скоуп


      1. hell0w0rd
        10.09.2015 09:02

        RR отказывается от контекстов в версии 1.0, на сколько мне известно.


  1. turbo_exe
    08.09.2015 13:49
    +1

    забавное решение, но одна из догм react это реиспользование компонентов. а это позволительно только в ситуации когда дочерние компоненты ничего не знают о родительских (i.e. не имеют зависимостей кроме props). flux придуман не просто так.


  1. Staltec
    08.09.2015 17:00

    Pub/Sub выглядит более гибким, и главное легальным, механизмом обмена данными между уровнями компонентов.


    1. webMarshal
      08.09.2015 17:04
      +2

      Бесспорно, но в глупых компонентах, которые в самом низу в иерархии не должно быть обращений к стору, поэтому хотя бы для таких компонентов контекст имеет место быть.


    1. erlyvideo
      08.09.2015 17:04
      +2

      PubSub? Это когда дочерний компонент лезет к глобальной переменной за регистрацией?


      1. turbo_exe
        08.09.2015 18:25
        +1

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


        1. erlyvideo
          08.09.2015 23:12

          Ну а как вы можете снаружи настроить, в какой паб-саб и по какому пабу и сабу компонент будет общаться?

          Паб-саб это в чистом виде глобальная переменная.


          1. turbo_exe
            09.09.2015 01:40

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


            1. erlyvideo
              09.09.2015 09:47

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

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

              Плюс к этому Flux выкидывает в помойку половину того, что есть в реакте, отказываясь от props


              1. aav
                09.09.2015 11:35
                +2

                Вам впору статью писать о плюсах/минусах AngularJS и React/Flux с точки зрения архитектуры приложения. Наконец-то может получиться интересный обзор от того, кто более-менее глубоко работал и с тем и с тем. А то обычно какой-то перекос, когда обзор пишет человек, который с одним инструментом работал долго и глубоко, а второй попытался освоить за недельку.


                1. erlyvideo
                  09.09.2015 15:34

                  Да, постараюсь =)

                  Мы и с тем, и с тем поработали.


              1. turbo_exe
                09.09.2015 14:30

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


                1. erlyvideo
                  09.09.2015 15:35

                  компонент сам заказывает, откуда ему взять PostsStore. Ему не передали его сверху, а он сам его нашел. Это и называется глобальная переменная


  1. erlyvideo
    08.09.2015 17:09
    +1

    Вы всё правильно написали, это хорошая штука.

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

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

    Как можно управлять поведением, если компонент сам шастает по глобальным переменным и чего-то там делает, я не понимаю.


    1. webMarshal
      08.09.2015 17:19

      Так компонент не должен шастать нигде, глупые компоненты управляются владельцем на основе props. PubSub не подходит тут.


      1. erlyvideo
        08.09.2015 17:20

        А как вы разделяете глупые и умные компоненты?


        1. webMarshal
          08.09.2015 17:22
          +1

          В разных папках держу) умные обращаются к стору, глупые получают данные через пропс и контекст.


          1. erlyvideo
            08.09.2015 17:29
            +1

            Тоже вариант, но это всё попытки ad-hoc придумать как же жить с реактовским подходом в реальной жизни.


          1. hell0w0rd
            08.09.2015 20:46
            -2

            Уважаемый, вы либо троллите, либо заблуждаетесь. Само понятие глупый подразумевает незнание. Чтобы получить что-то в контекст, это что-то нужно попросить.
            Глупый компонент — это компонент имеющий пропсы, редко стейт (на пример кастомный checkbox). Но никак не контекст.


            1. webMarshal
              08.09.2015 21:17

              Компонент, который в самом низу иерархии, но зависит от контекста — не является глупым? Данный в него передавать нужно через props или не пытаться иметь внизу иерархии глупый компонент?


      1. dfuse
        03.10.2015 00:07

        Подходит. Делаете глупый компонент, зависящий только от props, и умный, в котором делаете PubSub к чему угодно. Не обязательно все-все смешивать в одном компоненте.