Я переписываю свое приложение для использования 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
содержитUserAvatar
s людей, которые участвуют в этой категории; Article
Может иметь несколькоCategory
сек.
Поэтому, если бы я хотел получить все данные из магазинов на уровне Article
, мне нужно было бы:
- Получить статью из
ArticleStore
; - Получить все категории статей из
CategoryStore
; - Отдельно получить участников каждой категории из
UserStore
; - Каким-то образом передать все эти данные компонентам.
Еще более досадно то, что всякий раз, когда мне нужна глубоко вложенная сущность, мне нужно было бы добавить код на каждый уровень вложенности, чтобы дополнительно передать ее вниз.
Подведение итогов
Оба подхода кажутся ошибочными. Как мне решить эту проблему наиболее элегантно?
Мои цели:
У магазинов не должно быть безумного количества подписчиков. Глупо каждому
UserLink
слушать,UserStore
если родительские компоненты уже это делают.Если родительский компонент получил какой-либо объект из магазина (например
user
), я не хочу, чтобы какие-либо вложенные компоненты снова извлекали его. Я смогу передать это через реквизит.Мне не нужно извлекать все сущности (включая отношения) на верхнем уровне, потому что это усложнит добавление или удаление отношений. Я не хочу вводить новые свойства на всех уровнях вложенности каждый раз, когда вложенная сущность получает новое отношение (например, категория получает a
curator
).
источник
Ответы:
Большинство людей начинают с прослушивания соответствующих хранилищ в компоненте представления контроллера в верхней части иерархии.
Позже, когда будет казаться, что множество нерелевантных свойств передается по иерархии какому-то глубоко вложенному компоненту, некоторые люди решат, что это хорошая идея - позволить более глубокому компоненту отслеживать изменения в хранилищах. Это предлагает лучшую инкапсуляцию проблемной области, которой посвящена эта более глубокая ветвь дерева компонентов. Есть веские аргументы в пользу того, чтобы делать это разумно.
Однако я предпочитаю всегда слушать сверху и просто передавать все данные. Иногда я даже беру все состояние магазина и передаю его по иерархии как единый объект, и я буду делать это для нескольких магазинов. Таким образом, у меня была бы опора для состояния
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()
UserStore
Article
getArticles()
У вас также может возникнуть соблазн использовать
transferPropsTo()
, и многим людям это нравится, но я предпочитаю сохранять все явным для удобства чтения и, следовательно, удобства обслуживания.FWIW, насколько я понимаю, Дэвид Нолен использует аналогичный подход со своей структурой Om (которая в некоторой степени совместима с Flux ) с единственной точкой входа данных на корневом узле - эквивалент в Flux будет иметь только одно представление контроллера. слушаю все магазины. Это становится эффективным за счет использования
shouldComponentUpdate()
неизменяемых структур данных, которые можно сравнивать по ссылке с помощью ===. Если вам нужны неизменяемые структуры данных, обратитесь к David's mori или immutable-js от Facebook . Мои ограниченные знания об Om в основном получены из The Future of JavaScript MVC Frameworks.источник
Подход, к которому я пришел, заключается в том, что каждый компонент получает свои данные (а не идентификаторы) в качестве опоры. Если какому-либо вложенному компоненту требуется связанная сущность, его извлечение остается за родительским компонентом.
В нашем примере
Article
должна бытьarticle
опора, которая является объектом (предположительно полученным с помощью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> ) } });
Это делает самые внутренние компоненты глупыми, но не заставляет нас чертовски усложнять компоненты верхнего уровня.
источник
Мое решение намного проще. Каждому компоненту, имеющему собственное состояние, разрешено разговаривать и слушать магазины. Это очень похожие на контроллеры компоненты. Более глубокие вложенные компоненты, которые не поддерживают состояние, а просто визуализируют, не допускаются. Они получают реквизит только для чистого рендеринга, очень похожего на просмотр.
Таким образом, все перетекает из компонентов с сохранением состояния в компоненты без состояния. Низкое количество состояний.
В вашем случае статья будет с отслеживанием состояния и, следовательно, общается с магазинами, а UserLink и т. Д. Будет отображаться только так, чтобы получить article.user как опору.
источник
Проблемы, описанные в ваших двух философиях, являются общими для любого одностраничного приложения.
Они кратко обсуждаются в этом видео: https://www.youtube.com/watch?v=IrgHurBjQbg и Relay ( https://facebook.github.io/relay ) были разработаны Facebook для преодоления описанного вами компромисса.
Подход Relay очень ориентирован на данные. Это ответ на вопрос «Как мне получить только необходимые данные для каждого компонента в этом представлении в одном запросе к серверу?» И в то же время Relay гарантирует, что у вас будет небольшая связь по коду, когда компонент используется в нескольких представлениях.
Если Relay не подходит, вариант «Все компоненты объекта читают свои собственные данные» кажется мне лучшим подходом для описываемой вами ситуации. Я думаю, что неправильное представление о Flux - это то, что такое магазин. Концепция магазина существует не как место, где хранится модель или набор объектов. Хранилища - это временные места, куда ваше приложение помещает данные перед визуализацией представления. Настоящая причина их существования - решение проблемы зависимостей между данными, которые хранятся в разных хранилищах.
Что Flux не определяет, так это то, как магазин относится к концепции моделей и коллекции объектов (а-ля Backbone). В этом смысле некоторые люди фактически создают хранилище потоков - это место, где можно разместить коллекцию объектов определенного типа, которая не сбрасывается в течение всего времени, когда пользователь держит браузер открытым, но, насколько я понимаю, это не то, что хранилище должно быть.
Решение состоит в том, чтобы иметь другой уровень, на котором объекты, необходимые для рендеринга вашего представления (и, возможно, больше), хранятся и обновляются. Если вы используете этот слой, который абстрактно моделирует и коллекции, это не проблема, если вам, подкомпоненты, придется снова запрашивать их, чтобы получить свои собственные данные.
источник