На якому рівні вкладеності компоненти повинні читати сутності з магазинів у Flux?


89

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

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

Усі компоненти сутності зчитують власні дані

Кожен компонент, якому потрібні деякі дані з магазину, отримує лише ідентифікатор сутності та отримує сутність самостійно.
Так , наприклад, 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>
    )
  }
});

Недоліки цього підходу:

  • Розчаровує наявність 100-ти компонентів, які потенційно підписуються на магазини;
  • Це важко стежити за тим, як дані оновлюються і в якому порядку , оскільки кожен компонент витягує свої дані незалежно один від одного;
  • Незважаючи на те, що у вас вже є сутність у стані, ви змушені передати її ідентифікатор дітям, які знову отримають його (або порушать узгодженість).

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

Коли я втомився відстежувати помилки, я намагався поставити всі дані, що отримують, на найвищий рівень. Однак це виявилося неможливим, оскільки для деяких суб’єктів у мене є кілька рівнів вкладеності.

Наприклад:

  • A Categoryмістить UserAvatarлюдей, які сприяють цій категорії;
  • У одного Articleможе бути декількаCategory с.

Тому, якщо я хотів отримати всі дані з магазинів на рівні an Article, мені потрібно було б:

  • Отримати статтю з ArticleStore ;
  • Отримати всі категорії статті з CategoryStore ;
  • Окремо отримайте учасників кожної категорії UserStore ;
  • Якимось чином передайте всі ці дані компонентам.

Ще більш засмучує те, що коли мені потрібна глибоко вкладена сутність, мені потрібно буде додавати код на кожен рівень вкладеності, щоб додатково передавати його.

Підводячи підсумки

Обидва підходи здаються недосконалими. Як вирішити цю проблему найелегантніше?

Мої цілі:

  • Магазини не повинні мати шаленої кількості передплатників. Дурно для кожного UserLinkслухати, UserStoreякщо батьківські компоненти це вже роблять.

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

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


1
Дякуємо, що розмістили це. Я щойно опублікував тут дуже подібне запитання: groups.google.com/forum/ ... Усе, що я бачив, стосувалось плоских структур даних, а не пов’язаних даних. Я здивований, що не бачу найкращих практик щодо цього.
uberllama

Відповіді:


37

Більшість людей починають з прослуховування відповідних магазинів у компоненті подання контролера біля верхньої частини ієрархії.

Пізніше, коли здається, що безліч нерелевантних реквізитів передається через ієрархію якомусь глибоко вкладеному компоненту, деякі люди вирішать, що було б гарно дозволити більш глибокому компоненту прислухатися до змін у магазинах. Це забезпечує кращу інкапсуляцію проблемної області, про яку йдеться в цій глибшій гілці дерева компонентів. Існує вагомі аргументи, щоб зробити це розумно.

Однак я вважаю за краще завжди слухати зверху і просто передавати всі дані. Я іноді навіть приймаю весь стан магазину і передаю його через ієрархію як один об’єкт, і я буду робити це для кількох магазинів. Отже, я мав би опис для стану ArticleStore'', а інший - для стану UserStore'', і т.д. В іншому випадку я маю кілька джерел даних, і це може стати важким для налагодження.

З цією стратегією перевірка типу є складнішою, але ви можете встановити "фігуру" або шаблон типу для великого об'єкта-як-проп за допомогою Reap's PropTypes. Див .: 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()незмінних структур даних, які можна порівняти за посиланням із ===. Для незмінних структур даних перевірте mori Девіда або незмінний js Facebook . Моє обмежене знання про Om в основному походить від майбутнього JavaScript MVC Frameworks


4
Дякую за детальну відповідь! Я розглядав можливість передачі обох цілих знімків магазину. Однак це може спричинити проблему з перфомансом, оскільки кожне оновлення UserStore призведе до повторного відображення всіх статей на екрані, а PureRenderMixin не зможе визначити, які статті потрібно оновити, а які ні. Що стосується вашої другої пропозиції, я теж про це думав, але я не бачу, як я міг би зберегти рівність посилань на статті, якщо користувач статті "вводиться" під час кожного виклику getArticles (). Це знову могло б викликати у мене проблеми з перформансом.
Дан Абрамов

1
Я бачу вашу думку, але рішення є. З одного боку, ви можете зареєструвати shouldComponentUpdate (), якщо змінився масив ідентифікаторів користувачів для статті, який в основному просто прокручує функціональність та поведінку, які ви дійсно хочете в PureRenderMixin у цьому конкретному випадку.
fisherwebdev

3
Правильно, і в будь-якому випадку мені потрібно буде це робити лише тоді, коли я впевнений, що є проблеми з перфорацією, чого не повинно бути у магазинах, які оновлюються максимум кілька разів на хвилину. Ще раз дякую вам!
Дан Абрамов

8
Залежно від того, яку програму ви створюєте, збереження однієї точки входу зашкодить, як тільки на екрані з’явиться більше "віджетів", які безпосередньо не належать до одного кореня. Якщо ви з силою спробуєте зробити це таким чином, цей кореневий вузол матиме занадто велику відповідальність. KISS і SOLID, хоча це і сторона клієнта.
Rygu

37

Я підходив до того, що кожен компонент отримував свої дані (а не ідентифікатори) як опору. Якщо якомусь вкладеному компоненту потрібна пов’язана сутність, батьківський компонент повинен його отримати.

У нашому прикладі, Articleповинен мати articleprop, який є об'єктом (імовірно, отриманий 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>
    )
  }
});

Це робить внутрішні компоненти дурними, але не змушує нас ускладнювати компоненти верхнього рівня.


Просто щоб додати перспективу поверх цієї відповіді. Концептуально ви можете сформувати поняття власності, враховуючи, що збір даних фіналізується у верхній частині подання, хто насправді володіє даними (отже, прослуховування їх зберігання). Кілька прикладів: 1. AuthData має належати App Component, будь-яка зміна Auth повинна розповсюджуватися як компонент для всіх дочірніх компонентів App Component. 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 не вказує, так це те, як магазин співвідноситься з концепцією моделей та колекцією предметів (a la Backbone). У цьому сенсі деякі люди фактично роблять магазин флюсів місцем, куди можна помістити колекцію об’єктів певного типу, яка не змивається протягом усього часу, коли користувач тримає браузер відкритим, але, наскільки я розумію, флюс, це не те, що магазин передбачається.

Рішення полягає в тому, щоб мати ще один шар, де ви зберігаєте та оновлюєте сутності, необхідні для рендерінгу вашого перегляду (а можливо і більше). Якщо ви використовуєте цей шар, який абстрагує моделі та колекції, це не проблема, якщо вам, субкомпонентам, доведеться знову робити запит, щоб отримати власні дані.


1
Наскільки я розумію, підхід Дана щодо збереження різних типів об’єктів та посилання на них за допомогою ідентифікаторів дозволяє нам зберігати кеш цих об’єктів та оновлювати їх лише в одному місці. Як цього можна досягти, якщо, скажімо, у вас є кілька статей, що посилаються на одного користувача, а потім ім’я користувача оновлено? Єдиний спосіб - оновити всі статті новими даними користувачів. Це точно така сама проблема, з якою ви стикаєтесь, вибираючи між документом та реляційним db для вашого серверного сервера. Які ваші думки з цього приводу? Дякую.
Олексій Пономарьов
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.