Piszę ponownie moją aplikację, aby korzystała z Flux i mam problem z pobieraniem danych ze Sklepów. Mam dużo komponentów i dużo się zagnieżdżają. Niektóre z nich są duże ( Article
), inne małe i proste ( UserAvatar
, UserLink
).
Zmagałem się z tym, gdzie w hierarchii komponentów powinienem czytać dane ze Sklepów.
Wypróbowałem dwa ekstremalne podejścia, z których żadnego nie lubiłem:
Wszystkie komponenty jednostki odczytują własne dane
Każdy składnik, który potrzebuje niektórych danych ze sklepu, otrzymuje tylko identyfikator jednostki i samodzielnie pobiera jednostkę.
Na przykład, Article
jest przekazywana articleId
, UserAvatar
i UserLink
są przekazywane userId
.
Takie podejście ma kilka istotnych wad (omówionych w przykładowym kodzie).
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>
)
}
});
Wady tego podejścia:
- To frustrujące, że setki komponentów potencjalnie subskrybują Sklepy;
- To trudno śledzić, jak dane są aktualizowane i w jakiej kolejności , ponieważ każdy komponent pobiera swoje dane niezależnie;
- Nawet jeśli możesz już mieć jednostkę w stanie, jesteś zmuszony przekazać jej identyfikator dzieciom, które odzyskają go ponownie (lub zerwą spójność).
Wszystkie dane są odczytywane raz na najwyższym poziomie i przekazywane do komponentów
Kiedy byłem zmęczony śledzeniem błędów, próbowałem umieścić wszystkie dane na najwyższym poziomie. Okazało się to jednak niemożliwe, ponieważ dla niektórych podmiotów mam kilka poziomów zagnieżdżenia.
Na przykład:
- A
Category
zawieraUserAvatar
s osób, które współtworzą tę kategorię; Article
Może mieć kilkaCategory
s.
Dlatego gdybym chciał pobrać wszystkie dane ze Sklepów na poziomie an Article
, musiałbym:
- Pobierz artykuł z
ArticleStore
; - Pobierz wszystkie kategorie artykułów z
CategoryStore
; - Oddzielnie pobierz kontrybutorów każdej kategorii z
UserStore
; - W jakiś sposób przekazuje wszystkie te dane do komponentów.
Jeszcze bardziej frustrujące jest to, że ilekroć potrzebuję głęboko zagnieżdżonej jednostki, musiałbym dodać kod do każdego poziomu zagnieżdżenia, aby dodatkowo go przekazać.
Podsumowując
Oba podejścia wydają się wadliwe. Jak najbardziej elegancko rozwiązać ten problem?
Moje cele:
Sklepy nie powinny mieć szalonej liczby subskrybentów. To głupie, że każdy
UserLink
słucha,UserStore
jeśli komponenty nadrzędne już to robią.Jeśli komponent nadrzędny pobrał jakiś obiekt ze sklepu (np.
user
), Nie chcę, aby zagnieżdżone komponenty musiały pobierać go ponownie. Powinienem być w stanie przekazać to przez rekwizyty.Nie powinienem musieć pobierać wszystkich jednostek (w tym relacji) na najwyższym poziomie, ponieważ skomplikowałoby to dodawanie lub usuwanie relacji. Nie chcę wprowadzać nowych rekwizytów na wszystkich poziomach zagnieżdżenia za każdym razem, gdy zagnieżdżona encja otrzymuje nową relację (np. Kategoria otrzymuje a
curator
).
źródło
Odpowiedzi:
Większość ludzi zaczyna od słuchania odpowiednich sklepów w komponencie widoku kontrolera w pobliżu szczytu hierarchii.
Później, kiedy wydaje się, że wiele nieistotnych rekwizytów jest przekazywanych w hierarchii do jakiegoś głęboko zagnieżdżonego komponentu, niektórzy uznają, że dobrym pomysłem jest pozwolić, aby głębszy komponent nasłuchiwał zmian w sklepach. Zapewnia to lepszą hermetyzację domeny problemowej, której dotyczy ta głębsza gałąź drzewa komponentów. Istnieją dobre argumenty przemawiające za rozsądnym postępowaniem w tym zakresie.
Wolę jednak zawsze słuchać u góry i po prostu przekazywać wszystkie dane. Czasami nawet wezmę cały stan sklepu i przekażę go w hierarchii jako pojedynczy obiekt i zrobię to dla wielu sklepów. Więc miałbym propozycję dla stanu
ArticleStore
's, a drugą dla stanuUserStore
', itd. Uważam, że unikanie głęboko zagnieżdżonych widoków kontrolera utrzymuje pojedynczy punkt wejścia dla danych i ujednolica przepływ danych. W przeciwnym razie mam wiele źródeł danych i debugowanie może być trudne.W przypadku tej strategii sprawdzanie typów jest trudniejsze, ale można ustawić „kształt” lub szablon typu dla dużego obiektu jako propozycja za pomocą React's PropTypes. Zobacz: https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91 http://facebook.github.io/react/docs/reusable-components.html#prop -uprawomocnienie
Zauważ, że możesz chcieć umieścić logikę kojarzenia danych między sklepami w samych sklepach. Więc swojej
ArticleStore
potęgi i obejmują odpowiednich użytkowników z każdego rekordu przez to zapewnia . Robienie tego w swoich widokach brzmi jak wpychanie logiki do warstwy widoku, czego należy unikać, gdy tylko jest to możliwe.waitFor()
UserStore
Article
getArticles()
Możesz również pokusić się o skorzystanie z niego
transferPropsTo()
, a wiele osób lubi to robić, ale wolę zachować wszystko jawne dla czytelności, a tym samym łatwości utrzymania.FWIW, rozumiem, że David Nolen przyjmuje podobne podejście do swojego frameworka Om (który jest w pewnym stopniu kompatybilny z Flux ) z pojedynczym punktem wejścia danych w węźle głównym - odpowiednikiem we Flux byłoby mieć tylko jeden widok kontrolera słuchanie wszystkich sklepów. Jest to wydajne dzięki użyciu
shouldComponentUpdate()
i niezmiennych struktur danych, które można porównać przez odniesienie, z ===. Aby uzyskać niezmienne struktury danych, sprawdź mori Davida lub immutable-js Facebooka . Moja ograniczona wiedza na temat Om pochodzi głównie z The Future of JavaScript MVC Frameworksźródło
Podejście, do którego doszedłem, polega na tym, że każdy komponent otrzymuje dane (nie identyfikatory) jako rekwizyt. Jeśli jakiś zagnieżdżony komponent wymaga powiązanej encji, pobranie go zależy od komponentu nadrzędnego.
W naszym przykładzie
Article
powinien miećarticle
rekwizyt, który jest obiektem (prawdopodobnie pobranym przezArticleList
lubArticlePage
).Ponieważ
Article
również chce renderowaćUserLink
iUserAvatar
dla autora artykułu, zasubskrybujeUserStore
i zachowaauthor: UserStore.get(article.authorId)
swój stan. Będzie wtedy renderowaćUserLink
izUserAvatar
tymthis.state.author
. Jeśli chcą to przekazać dalej, mogą. Żadne komponenty podrzędne nie będą musiały ponownie pobierać tego użytkownika.Powtarzać:
To całkiem dobrze rozwiązuje mój problem. Przykład kodu przepisany z użyciem tego podejścia:
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> ) } });
To sprawia, że najbardziej wewnętrzne komponenty stają się głupie, ale nie zmusza nas do komplikowania piekła z komponentów najwyższego poziomu.
źródło
Moje rozwiązanie jest znacznie prostsze. Każdy komponent, który ma swój własny stan, może rozmawiać i słuchać sklepów. Są to elementy bardzo podobne do kontrolerów. Głębsze zagnieżdżone komponenty, które nie zachowują stanu, ale tylko renderują, są niedozwolone. Otrzymują tylko rekwizyty do czystego renderowania, bardzo podobne do widoku.
W ten sposób wszystko przepływa od komponentów stanowych do komponentów bezstanowych. Utrzymywanie niskiego stanu liczenia.
W twoim przypadku artykuł byłby stanowy i dlatego rozmawiałby ze sklepami, a UserLink itp. Wyrenderowałby się tylko w taki sposób, że otrzymałby article.user jako prop.
źródło
Problemy opisane w twoich dwóch filozofiach są wspólne dla każdej aplikacji jednostronicowej.
Zostały one krótko omówione w tym filmie: https://www.youtube.com/watch?v=IrgHurBjQbg i Relay ( https://facebook.github.io/relay ) został opracowany przez Facebooka, aby przezwyciężyć kompromisy, które opisujesz.
Podejście Relay jest bardzo skoncentrowane na danych. Jest to odpowiedź na pytanie „Jak w jednym zapytaniu do serwera uzyskać tylko potrzebne dane dla każdego komponentu w tym widoku?” Jednocześnie Relay zapewnia niewielkie sprzężenie w kodzie, gdy komponent jest używany w wielu widokach.
Jeśli Przekaźnik nie jest opcją , „Wszystkie komponenty encji odczytują swoje własne dane” wydaje mi się lepszym podejściem do opisywanej sytuacji. Myślę, że błędne przekonanie we Flux jest tym, czym jest sklep. Pojęcie sklepu nie istnieje, aby być miejscem, w którym przechowywany jest model lub kolekcja przedmiotów. Magazyny to tymczasowe miejsca, w których aplikacja umieszcza dane przed renderowaniem widoku. Prawdziwym powodem ich istnienia jest rozwiązanie problemu zależności między danymi, które trafiają do różnych sklepów.
Flux nie określa, jak sklep odnosi się do koncepcji modeli i kolekcji obiektów (a la Backbone). W tym sensie niektórzy ludzie faktycznie tworzą flux store miejsce, w którym można umieścić kolekcję obiektów określonego typu, która nie jest pusta przez cały czas, gdy użytkownik utrzymuje otwartą przeglądarkę, ale, jak rozumiem flux, nie jest to tym, co sklep to powinno być.
Rozwiązaniem jest posiadanie kolejnej warstwy, w której elementy niezbędne do renderowania widoku (i potencjalnie więcej) są przechowywane i aktualizowane. Jeśli masz tę warstwę, która abstrakcyjnie modele i kolekcje, nie jest problemem, jeśli podskładniki muszą ponownie zapytać, aby uzyskać własne dane.
źródło