Na jakim poziomie zagnieżdżenia komponenty powinny odczytywać elementy ze sklepów we Flux?

89

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, Articlejest przekazywana articleId, UserAvatari UserLinksą 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 Categoryzawiera UserAvatars osób, które współtworzą tę kategorię;
  • ArticleMoże mieć kilka Categorys.

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 UserLinksłucha, UserStorejeś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).

Dan Abramov
źródło
1
Dzięki za opublikowanie tego. Właśnie opublikowałem tutaj bardzo podobne pytanie: groups.google.com/forum/… Wszystko, co widziałem, dotyczyło raczej płaskich struktur danych niż powiązanych danych. Dziwię się, że nie widzę w tym zakresie najlepszych praktyk.
uberllama

Odpowiedzi:

37

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 stanu UserStore', 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 ArticleStorepotę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()UserStoreArticlegetArticles()

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

fisherwebdev
źródło
4
Dziękuję za szczegółową odpowiedź! Rozważałem przekazanie obu całych migawek stanu sklepów. Może to jednak powodować problem z perfekcją, ponieważ każda aktualizacja UserStore spowoduje ponowne renderowanie wszystkich artykułów na ekranie, a PureRenderMixin nie będzie w stanie stwierdzić, które artykuły wymagają aktualizacji, a które nie. Jeśli chodzi o twoją drugą sugestię, również o tym myślałem, ale nie widzę, jak mogę zachować równość odwołań do artykułu, jeśli użytkownik artykułu jest „wstrzykiwany” w każdym wywołaniu getArticles (). To znowu potencjalnie dałoby mi problemy z perfekcją.
Dan Abramov,
1
Rozumiem twój punkt widzenia, ale są rozwiązania. Po pierwsze, możesz sprawdzić shouldComponentUpdate (), jeśli tablica identyfikatorów użytkowników dla artykułu uległa zmianie, co jest po prostu ręcznym przełączaniem funkcjonalności i zachowania, które naprawdę chcesz w PureRenderMixin w tym konkretnym przypadku.
fisherwebdev
3
Tak, i tak musiałbym to zrobić tylko wtedy, gdy jestem pewien, że problemy z perfekcją, co nie powinno mieć miejsca w przypadku sklepów, które aktualizują się maksymalnie kilka razy na minutę. Jeszcze raz dziękuję!
Dan Abramov,
8
W zależności od rodzaju aplikacji, którą tworzysz, utrzymanie jednego punktu wejścia może zaszkodzić, gdy tylko zaczniesz mieć więcej „widżetów” na ekranie, które nie należą bezpośrednio do tego samego katalogu głównego. Jeśli na siłę spróbujesz to zrobić w ten sposób, ten węzeł główny będzie miał zbyt dużą odpowiedzialność. KISS i SOLID, mimo że jest to strona klienta.
Rygu
37

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 Articlepowinien mieć articlerekwizyt, który jest obiektem (prawdopodobnie pobranym przez ArticleListlub ArticlePage).

Ponieważ Articlerównież chce renderować UserLinki UserAvatardla autora artykułu, zasubskrybuje UserStorei zachowa author: UserStore.get(article.authorId)swój stan. Będzie wtedy renderować UserLinkiz UserAvatartym this.state.author. Jeśli chcą to przekazać dalej, mogą. Żadne komponenty podrzędne nie będą musiały ponownie pobierać tego użytkownika.

Powtarzać:

  • Żaden komponent nigdy nie otrzymuje ID jako rekwizytu; wszystkie komponenty otrzymują odpowiednie obiekty.
  • Jeśli komponenty potomne potrzebują bytu, to rodzic jest odpowiedzialny za pobranie go i przekazanie jako rekwizyt.

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.

Dan Abramov
źródło
Wystarczy dodać perspektywę do tej odpowiedzi. Koncepcyjnie możesz rozwinąć pojęcie własności, biorąc pod uwagę gromadzenie danych, sfinalizuj na najwyższym widoku, kto faktycznie jest właścicielem danych (stąd słuchanie jego magazynu). Kilka przykładów: 1. AuthData powinno być własnością komponentu aplikacji, każda zmiana w Auth powinna być propagowana jako rekwizyty dla wszystkich elementów podrzędnych przez komponent aplikacji. 2. ArticleData powinien być własnością ArticlePage lub Article List, zgodnie z sugestią zawartą w odpowiedzi. teraz każdy element podrzędny ArticleList może otrzymywać AuthData i / lub ArticleData z ArticleList, niezależnie od tego, który składnik faktycznie pobrał te dane.
Saumitra R. Bhave
2

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.

Rygu
źródło
1
Przypuszczam, że zadziałałoby to, gdyby artykuł miał właściwość obiektu użytkownika, ale w moim przypadku ma tylko identyfikator użytkownika (ponieważ prawdziwy użytkownik jest przechowywany w UserStore). A może nie rozumiem? Czy mógłbyś podzielić się przykładem?
Dan Abramov,
Zainicjuj pobieranie dla użytkownika w artykule. Lub zmień zaplecze interfejsu API, aby identyfikator użytkownika był „rozwijany”, aby uzyskać użytkownika od razu. W każdym przypadku głębiej zagnieżdżone komponenty zachowaj proste widoki i polegaj tylko na rekwizytach.
Rygu,
2
Nie stać mnie na zainicjowanie pobierania w artykule, ponieważ trzeba go razem wyrenderować. Jeśli chodzi o rozszerzalne interfejsy API, kiedyś je mieliśmy, ale ich analiza ze sklepów jest bardzo problematyczna. Z tego właśnie powodu napisałem bibliotekę, aby spłaszczyć odpowiedzi.
Dan Abramov,
0

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.

Chris Cinelli
źródło
1
O ile rozumiem, podejście Dana do utrzymywania różnych typów obiektów i odwoływania się do nich za pomocą identyfikatorów pozwala nam przechowywać te obiekty w pamięci podręcznej i aktualizować je tylko w jednym miejscu. Jak można to osiągnąć, jeśli, powiedzmy, masz kilka artykułów, które odnoszą się do tego samego użytkownika, a następnie nazwa użytkownika jest aktualizowana? Jedynym sposobem jest zaktualizowanie wszystkich artykułów o nowe dane użytkownika. To jest dokładnie ten sam problem, który pojawia się przy wyborze między dokumentem a relacyjną bazą danych dla zaplecza. Co o tym myślisz? Dziękuję Ci.
Alex Ponomarev