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

Я переписываю свое приложение для использования Flux, и у меня есть проблема с извлечением данных из Stores. У меня много компонентов, и они много гнездятся. Некоторые из них большие (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>
    )
  }
});

Недостатки этого подхода:

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

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

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

Например:

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

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

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

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

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

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

Мои цели:

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

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

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

Ответ 1

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

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

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

Повторить:

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

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

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

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

Ответ 2

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

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

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

Проверка типов сложнее с этой стратегией, но вы можете настроить "форму" или шаблон типа для большого объекта с помощью React PropTypes. Видеть: 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-совместимая) с единственной точкой входа данных на root node - эквивалент в Flux должен был иметь только один просмотр контроллера, который будет прослушивать все магазины. Это достигается с помощью shouldComponentUpdate() и неизменяемых структур данных, которые можно сравнить по ссылке, с ===. Для неизменяемых структур данных вы можете просмотреть David mori или Facebook immutable-js. Мое ограниченное знание Om в основном происходит от Будущее JavaScript MVC Framework

Ответ 3

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

Таким образом, все происходит от компонентов состояния до состояния безстоящих компонентов. Хранение данных состояния считается низким.

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

Ответ 4

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

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

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

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

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

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