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

89

Я переписываю свое приложение для использования 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>
    )
  }
});

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

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

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

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

Например:

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

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

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

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

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

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

Мои цели:

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

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

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

Дан Абрамов
источник
1
Спасибо, что разместили это. Я только что разместил здесь очень похожий вопрос: groups.google.com/forum/… Все, что я видел, касалось плоских структур данных, а не связанных данных. Я удивлен, что не вижу в этом никаких лучших практик.
uberllama

Ответы:

37

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

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

Однако я предпочитаю всегда слушать сверху и просто передавать все данные. Иногда я даже беру все состояние магазина и передаю его по иерархии как единый объект, и я буду делать это для нескольких магазинов. Таким образом, у меня была бы опора для состояния ArticleStore's, а также другая для состояния 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 -Проверка

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

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

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

рыбак
источник
4
Спасибо за подробный ответ! Я действительно рассматривал возможность передачи обоих снимков состояния магазинов целиком. Однако это может вызвать у меня проблемы с перфомансом, потому что каждое обновление UserStore будет вызывать повторную визуализацию всех статей на экране, и PureRenderMixin не сможет определить, какие статьи нужно обновить, а какие нет. Что касается вашего второго предложения, я тоже об этом подумал, но я не понимаю, как я могу сохранить равенство ссылок на статьи, если пользователь статьи «вводится» при каждом вызове getArticles (). Это снова потенциально может вызвать у меня проблемы с производительностью.
Дэн Абрамов
1
Я понимаю вашу точку зрения, но есть решения. Во-первых, вы можете проверить в shouldComponentUpdate (), если массив идентификаторов пользователей для статьи изменился, что в основном просто ручное изменение функциональности и поведения, которые вам действительно нужны в PureRenderMixin в этом конкретном случае.
fisherwebdev
3
Верно, и в любом случае мне нужно будет делать это только тогда, когда я уверен, что есть проблемы с перфомансом, которых не должно быть в магазинах, которые обновляются максимум несколько раз в минуту. Еще раз спасибо!
Дэн Абрамов
8
В зависимости от того, какое приложение вы создаете, сохранение одной единственной точки входа может повредить, как только у вас появится больше «виджетов» на экране, которые не принадлежат непосредственно к одному корню. Если вы попытаетесь сделать это таким образом, этот корневой узел будет нести слишком большую ответственность. KISS и SOLID, хоть это и клиентская сторона.
Rygu 08
37

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

В нашем примере 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>
    )
  }
});

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

Дан Абрамов
источник
Просто чтобы добавить перспективу к этому ответу. Концептуально вы можете разработать понятие владения, учитывая, что сбор данных завершается в самом верхнем представлении, кто на самом деле владеет данными (следовательно, прослушивает его хранилище). Несколько примеров: 1. AuthData должен принадлежать компоненту приложения. Любое изменение в Auth должно распространяться вниз как реквизит для всех дочерних элементов с помощью компонента приложения. 2. ArticleData должна принадлежать ArticlePage или Article List, как предложено в ответе. теперь любой дочерний элемент ArticleList может получать AuthData и / или ArticleData из ArticleList независимо от того, какой компонент фактически получил эти данные.
Саумитра Р. Бхаве
2

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

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

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

Рыгу
источник
1
Я полагаю, это сработало бы, если бы статья имела свойство объекта пользователя, но в моем случае у него есть только userId (потому что настоящий пользователь хранится в UserStore). Или я упустил вашу точку зрения? Не могли бы вы привести пример?
Дэн Абрамов
Инициируйте выборку для пользователя в статье. Или измените свой API-интерфейс, чтобы сделать идентификатор пользователя «расширяемым», чтобы вы сразу получали пользователя. В любом случае сохраняйте более глубокие вложенные компоненты в простых представлениях и полагайтесь только на свойства.
Rygu
2
Я не могу позволить себе инициировать выборку в статье, потому что ее нужно визуализировать вместе. Что касается расширяемых API , мы использовали , чтобы иметь их, но они очень проблематично разобрать из магазинов. Я даже написал библиотеку для сглаживания ответов именно по этой причине.
Дэн Абрамов
0

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

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

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

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

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

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

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