На каком уровне вложенности компоненты должны считывать объекты из Stores in Flux?

Я переписываю свое приложение для использования Flux, и у меня проблема с получением данных из магазинов. У меня много компонентов, и они очень много вложены. Некоторые из них большие (Article), некоторые маленькие и простые (UserAvatar, UserLink).

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

Все компоненты сущности читают свои собственные данные

Каждый компонент, которому нужны данные из Store, получает только идентификатор объекта и извлекает объект самостоятельно.
Например, Article передается articleId, UserAvatar и UserLink передаются userId.

У этого подхода есть несколько существенных недостатков (обсуждаемых в примере кода).

var Article = React.createClass({
  mixins: [createStoreMixin(ArticleStore)],

  propTypes: {
    articleId: PropTypes.number.isRequired
  },

  getStateFromStores() {
    return {
      article: ArticleStore.get(this.props.articleId);
    }
  },

  render() {
    var article = this.state.article,
        userId = article.userId;

    return (
      <div>
        <UserLink userId={userId}>
          <UserAvatar userId={userId} />
        </UserLink>

        <h1>{article.title}</h1>
        <p>{article.text}</p>

        <p>Read more by <UserLink userId={userId} />.</p>
      </div>
    )
  }
});

var UserAvatar = React.createClass({
  mixins: [createStoreMixin(UserStore)],

  propTypes: {
    userId: PropTypes.number.isRequired
  },

  getStateFromStores() {
    return {
      user: UserStore.get(this.props.userId);
    }
  },

  render() {
    var user = this.state.user;

    return (
      <img src={user.thumbnailUrl} />
    )
  }
});

var UserLink = React.createClass({
  mixins: [createStoreMixin(UserStore)],

  propTypes: {
    userId: PropTypes.number.isRequired
  },

  getStateFromStores() {
    return {
      user: UserStore.get(this.props.userId);
    }
  },

  render() {
    var user = this.state.user;

    return (
      <Link to='user' params={{ userId: this.props.userId }}>
        {this.props.children || user.name}
      </Link>
    )
  }
});

Минусы этого подхода:

  • Очень неприятно иметь сотни компонентов, потенциально подписывающихся на магазины;
  • Трудно отслеживать, как и в каком порядке обновляются данные, потому что каждый компонент извлекает свои данные независимо;
  • Даже если у вас может уже быть сущность в состоянии, вы вынуждены передать ее идентификатор дочерним элементам, которые получат ее снова (или нарушат согласованность).

Все данные считываются один раз на верхнем уровне и передаются компонентам.

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

Например:

  • Category содержит UserAvatar людей, которые вносят свой вклад в эту категорию;
  • Article может иметь несколько Category.

Поэтому, если бы я хотел получить все данные из Магазинов на уровне Article, мне нужно было бы:

  • Получить статью из ArticleStore;
  • Получить все категории статей из CategoryStore;
  • Отдельно получить участников каждой категории из UserStore;
  • Каким-то образом передать все эти данные компонентам.

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

Подведение итогов

Оба подхода кажутся ошибочными. Как мне решить эту проблему наиболее элегантно?

Мои цели:

  • У магазинов не должно быть безумного количества подписчиков. Глупо для каждого UserLink слушать UserStore, если родительские компоненты уже это делают.

  • Если родительский компонент получил какой-либо объект из магазина (например, user), я не хочу, чтобы какие-либо вложенные компоненты снова извлекали его. Я смогу передать это через реквизит.

  • Мне не нужно извлекать все сущности (включая отношения) на верхнем уровне, потому что это усложнит добавление или удаление отношений. Я не хочу вводить новые реквизиты на всех уровнях вложенности каждый раз, когда вложенная сущность получает новое отношение (например, категория получает curator).


person Dan Abramov    schedule 06.09.2014    source источник
comment
Спасибо, что разместили это. Я только что разместил здесь очень похожий вопрос: groups .google.com / forum / Все, что я видел, касалось плоских структур данных, а не связанных данных. Я удивлен, что не вижу в этом никаких лучших практик.   -  person uberllama    schedule 09.01.2015


Ответы (4)


Большинство людей начинают с прослушивания соответствующих хранилищ в компоненте представления контроллера в верхней части иерархии.

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

Однако я предпочитаю всегда слушать сверху и просто передавать все данные. Иногда я даже беру все состояние магазина и передаю его по иерархии как единый объект, и я буду делать это для нескольких магазинов. Таким образом, у меня будет опора для состояния ArticleStore, а другая для состояния UserStore и т. Д. Я считаю, что избегание глубоко вложенных представлений контроллера поддерживает единственную точку входа для данных и унифицирует поток данных. В противном случае у меня есть несколько источников данных, и это может стать трудным для отладки.

С этой стратегией проверка типов усложняется, но вы можете настроить «форму» или шаблон типа для большого объекта как свойства с помощью свойства PropTypes React. См. https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91 http://facebook.github.io/react/docs/reusable-components.html#prop-validation

Обратите внимание, что вы можете захотеть поместить логику связывания данных между хранилищами в сами магазины. Таким образом, ваш ArticleStore может waitFor() UserStore и включать соответствующих пользователей с каждой Article записью, которую он предоставляет через getArticles(). Выполнение этого в ваших представлениях звучит как вставка логики в слой представления, чего следует избегать, когда это возможно.

У вас также может возникнуть соблазн использовать transferPropsTo(), и многим людям это нравится, но я предпочитаю сохранять все явным для удобства чтения и, следовательно, удобства обслуживания.

FWIW, насколько я понимаю, Дэвид Нолен использует аналогичный подход со своей структурой Om (это в некоторой степени совместимый с Flux) с единственной точкой входа данных на корневом узле - эквивалентом в Flux было бы наличие только одного представления контроллера, прослушивающего все хранилища. Это становится эффективным за счет использования shouldComponentUpdate() и неизменяемых структур данных, которые можно сравнивать по ссылке с помощью ===. Для неизменяемых структур данных проверьте mori Дэвида или immutable-js. Мои ограниченные знания об Om в основном получены из Будущее JavaScript-фреймворков MVC

person fisherwebdev    schedule 07.09.2014
comment
Спасибо за развернутый ответ! Я действительно рассматривал возможность передачи обоих снимков состояния магазинов целиком. Однако это может вызвать у меня проблемы с перфомансом, потому что каждое обновление UserStore будет вызывать повторную визуализацию всех статей на экране, и PureRenderMixin не сможет определить, какие статьи нужно обновить, а какие нет. Что касается вашего второго предложения, я тоже об этом подумал, но я не понимаю, как я могу сохранить равенство ссылок на статьи, если пользователь статьи вводится при каждом вызове getArticles (). Это снова потенциально может вызвать у меня проблемы с производительностью. - person Dan Abramov; 07.09.2014
comment
Я понимаю вашу точку зрения, но есть решения. Во-первых, вы можете проверить в shouldComponentUpdate (), если массив идентификаторов пользователей для статьи изменился, что в основном представляет собой ручную настройку функциональности и поведения, которые вам действительно нужны в PureRenderMixin в этом конкретном случае. - person fisherwebdev; 07.09.2014
comment
Верно, и в любом случае мне нужно будет делать это только тогда, когда я уверен, что есть есть проблемы с перфомансом, которых не должно быть в магазинах, которые обновляются максимум несколько раз в минуту. Еще раз спасибо! - person Dan Abramov; 07.09.2014
comment
В зависимости от того, какое приложение вы создаете, сохранение одной единственной точки входа может повредить, как только вы начнете отображать на экране больше виджетов, которые напрямую не принадлежат одному корню. Если вы попытаетесь сделать это таким образом, этот корневой узел будет нести слишком большую ответственность. KISS и SOLID, хоть это и клиентская сторона. - person Rygu; 08.09.2014

Подход, к которому я пришел, заключается в том, что каждый компонент получает свои данные (а не идентификаторы) в качестве опоры. Если какому-либо вложенному компоненту требуется связанная сущность, то его извлечение зависит от родительского компонента.

В нашем примере Article должен иметь свойство article, которое является объектом (предположительно полученным ArticleList или ArticlePage).

Поскольку Article также хочет отображать UserLink и UserAvatar для автора статьи, он подписывается на UserStore и сохраняет author: UserStore.get(article.authorId) в своем состоянии. Затем он будет отображать UserLink и UserAvatar с этим this.state.author. Если они хотят передать это дальше, они могут. Никаким дочерним компонентам не потребуется повторно получать этого пользователя.

Повторить:

  • Ни один компонент никогда не получает ID как опору; все компоненты получают свои соответствующие объекты.
  • Если дочерним компонентам требуется сущность, ответственность за ее получение и передачу в качестве опоры лежит на родителях.

Это неплохо решает мою проблему. Пример кода, переписанный для использования этого подхода:

var Article = React.createClass({
  mixins: [createStoreMixin(UserStore)],

  propTypes: {
    article: PropTypes.object.isRequired
  },

  getStateFromStores() {
    return {
      author: UserStore.get(this.props.article.authorId);
    }
  },

  render() {
    var article = this.props.article,
        author = this.state.author;

    return (
      <div>
        <UserLink user={author}>
          <UserAvatar user={author} />
        </UserLink>

        <h1>{article.title}</h1>
        <p>{article.text}</p>

        <p>Read more by <UserLink user={author} />.</p>
      </div>
    )
  }
});

var UserAvatar = React.createClass({
  propTypes: {
    user: PropTypes.object.isRequired
  },

  render() {
    var user = this.props.user;

    return (
      <img src={user.thumbnailUrl} />
    )
  }
});

var UserLink = React.createClass({
  propTypes: {
    user: PropTypes.object.isRequired
  },

  render() {
    var user = this.props.user;

    return (
      <Link to='user' params={{ userId: this.props.user.id }}>
        {this.props.children || user.name}
      </Link>
    )
  }
});

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

person Dan Abramov    schedule 06.09.2014
comment
Просто чтобы добавить перспективу к этому ответу. Концептуально вы можете развить понятие владения, учитывая, что сбор данных завершается в самом верхнем представлении, кто на самом деле владеет данными (следовательно, прослушивает его хранилище). Несколько примеров: 1. AuthData должен принадлежать компоненту приложения. Любое изменение в Auth должно распространяться вниз как реквизит для всех дочерних элементов с помощью компонента приложения. 2. ArticleData должна принадлежать ArticlePage или Article List, как предложено в ответе. теперь любой дочерний элемент ArticleList может получать AuthData и / или ArticleData из ArticleList независимо от того, какой компонент фактически получил эти данные. - person Saumitra R. Bhave; 18.10.2015

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

Таким образом, все перетекает из компонентов с сохранением состояния в компоненты без состояния. Удержание количества состояний на низком уровне.

В вашем случае статья будет с отслеживанием состояния и, следовательно, общается с магазинами, а UserLink и т. Д. Будет отображаться только так, чтобы получить article.user в качестве опоры.

person Rygu    schedule 06.09.2014
comment
Я полагаю, это сработало бы, если бы у статьи было свойство объекта пользователя, но в моем случае у него есть только userId (потому что реальный пользователь хранится в UserStore). Или я упустил вашу точку зрения? Не могли бы вы привести пример? - person Dan Abramov; 06.09.2014
comment
Инициируйте выборку для пользователя в статье. Или измените свой API-интерфейс, чтобы сделать идентификатор пользователя расширяемым, чтобы вы сразу получали пользователя. В любом случае сохраняйте более глубокие вложенные компоненты в простых представлениях и полагайтесь только на реквизиты. - person Rygu; 07.09.2014
comment
Я не могу позволить себе инициировать выборку в статье, потому что ее нужно обрабатывать вместе. Что касается расширяемых API, мы использовали, чтобы иметь их, но их очень проблематично разобрать из Магазинов. Я даже написал библиотеку для сглаживания ответов именно по этой причине. - person Dan Abramov; 07.09.2014

Проблемы, описанные в ваших двух философиях, являются общими для любого одностраничного приложения.

Вкратце они обсуждаются в этом видео: https://www.youtube.com/watch?v=IrgHurBjQbg и Relay (https://facebook.github.io/relay) были разработаны Facebook. чтобы преодолеть описанный вами компромисс.

Подход Relay очень ориентирован на данные. Это ответ на вопрос «Как мне получить только необходимые данные для каждого компонента в этом представлении в одном запросе к серверу?» И в то же время Relay гарантирует, что у вас будет небольшая связь по коду, когда компонент используется в нескольких представлениях.

Если Relay не доступен, вариант «Все компоненты объекта читают свои собственные данные» кажется мне лучшим подходом для описываемой вами ситуации. Я думаю, что неправильное представление о Flux - это то, что такое магазин. Концепция магазина существует не как место, где хранится модель или набор объектов. Хранилища - это временные места, куда ваше приложение помещает данные перед визуализацией представления. Настоящая причина, по которой они существуют, - это решение проблемы зависимостей между данными, которые хранятся в разных хранилищах.

Что Flux не определяет, так это то, как магазин относится к концепции моделей и коллекции объектов (а-ля Backbone). В этом смысле некоторые люди фактически создают хранилище потоков - это место, где можно разместить коллекцию объектов определенного типа, которая не сбрасывается в течение всего времени, когда пользователь держит браузер открытым, но, насколько я понимаю, это не то, что хранилище должно быть.

Решение состоит в том, чтобы иметь еще один уровень, на котором объекты, необходимые для рендеринга вашего представления (и, возможно, больше), хранятся и обновляются. Если вы используете этот слой, который представляет собой абстрактные модели и коллекции, это не проблема, если вы, подкомпоненты, должны снова запросить свои собственные данные.

person Chris Cinelli    schedule 16.10.2015
comment
Насколько я понимаю, подход Дэна к хранению различных типов объектов и обращению к ним через идентификаторы позволяет нам хранить кеш этих объектов и обновлять их только в одном месте. Как этого добиться, если, скажем, у вас есть несколько статей, которые ссылаются на одного и того же пользователя, а затем имя пользователя обновляется? Единственный способ - обновить все статьи новыми пользовательскими данными. Это точно такая же проблема, с которой вы сталкиваетесь при выборе между документом и реляционной базой данных для вашего бэкэнда. Что ты думаешь об этом? Спасибо. - person Alex Ponomarev; 24.11.2015