Pada tingkat bersarang apa komponen harus membaca entitas dari Stores in Flux?

89

Saya menulis ulang aplikasi saya untuk menggunakan Flux dan saya memiliki masalah dengan mengambil data dari Toko. Saya memiliki banyak komponen, dan mereka banyak bersarang. Beberapa di antaranya besar ( Article), beberapa kecil dan sederhana ( UserAvatar, UserLink).

Saya telah berjuang dengan di mana dalam hierarki komponen saya harus membaca data dari Toko.
Saya mencoba dua pendekatan ekstrim, yang tidak satu pun yang saya sukai:

Semua komponen entitas membaca datanya sendiri

Setiap komponen yang membutuhkan beberapa data dari Store hanya menerima ID entitas dan mengambil entitasnya sendiri.
Misalnya, Articledilewatkan articleId, UserAvatardan UserLinkdilewatkan userId.

Pendekatan ini memiliki beberapa kelemahan signifikan (dibahas di bawah contoh kode).

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>
    )
  }
});

Kerugian dari pendekatan ini:

  • Sungguh membuat frustrasi memiliki 100-an komponen yang berpotensi berlangganan Toko;
  • Ini sulit untuk melacak bagaimana data diperbarui dan dalam rangka apa karena masing-masing komponen mengambil data secara independen;
  • Meskipun Anda mungkin sudah memiliki entitas dalam status, Anda terpaksa meneruskan ID-nya kepada anak-anak, yang akan mengambilnya lagi (atau merusak konsistensi).

Semua data dibaca sekali di tingkat teratas dan diteruskan ke komponen

Ketika saya lelah melacak bug, saya mencoba menempatkan semua pengambilan data di tingkat atas. Ini, bagaimanapun, terbukti tidak mungkin karena untuk beberapa entitas saya memiliki beberapa tingkat bersarang.

Sebagai contoh:

  • A Categoryberisi UserAvatarorang yang berkontribusi pada kategori itu;
  • Sebuah Articlemungkin memiliki beberapa Categorys.

Oleh karena itu, jika saya ingin mengambil semua data dari Toko di tingkat Article, saya perlu:

  • Ambil artikel dari ArticleStore;
  • Ambil semua kategori artikel dari CategoryStore;
  • Ambil secara terpisah setiap kontributor kategori dari UserStore;
  • Entah bagaimana meneruskan semua data itu ke komponen.

Yang lebih membuat frustrasi, setiap kali saya membutuhkan entitas yang sangat bersarang, saya perlu menambahkan kode ke setiap tingkat bersarang untuk meneruskannya.

Menyimpulkan

Kedua pendekatan tersebut tampaknya cacat. Bagaimana cara mengatasi masalah ini dengan paling elegan?

Tujuan saya:

  • Toko seharusnya tidak memiliki jumlah pelanggan yang gila-gilaan. Itu bodoh bagi masing-masing UserLinkuntuk mendengarkan UserStorejika komponen induk sudah melakukan itu.

  • Jika komponen induk telah mengambil beberapa objek dari penyimpanan (misalnya user), saya tidak ingin komponen bersarang harus mengambilnya lagi. Saya harus bisa meneruskannya melalui alat peraga.

  • Saya tidak perlu mengambil semua entitas (termasuk hubungan) di tingkat atas karena akan mempersulit penambahan atau penghapusan hubungan. Saya tidak ingin memperkenalkan properti baru di semua level bersarang setiap kali entitas bersarang mendapat hubungan baru (misalnya kategori mendapat a curator).

Dan Abramov
sumber
1
Terima kasih telah memposting ini. Saya baru saja memposting pertanyaan yang sangat mirip di sini: groups.google.com/forum/… Semua yang saya lihat berhubungan dengan struktur data datar daripada data terkait. Saya terkejut tidak melihat praktik terbaik tentang ini.
uberllama

Jawaban:

37

Kebanyakan orang memulai dengan mendengarkan penyimpanan yang relevan dalam komponen tampilan pengontrol di dekat bagian atas hierarki.

Nanti, ketika tampaknya banyak props yang tidak relevan diturunkan melalui hierarki ke beberapa komponen bersarang yang dalam, beberapa orang akan memutuskan bahwa sebaiknya biarkan komponen yang lebih dalam mendengarkan perubahan di penyimpanan. Ini menawarkan enkapsulasi yang lebih baik dari domain masalah yang merupakan cabang lebih dalam dari pohon komponen ini. Ada argumen bagus yang harus dibuat untuk melakukan ini dengan bijaksana.

Namun, saya lebih suka untuk selalu mendengarkan di atas dan hanya meneruskan semua data. Saya kadang-kadang bahkan akan mengambil seluruh negara bagian toko dan menyebarkannya melalui hierarki sebagai satu objek, dan saya akan melakukan ini untuk beberapa toko. Jadi saya akan memiliki prop untuk ArticleStorestatus, dan satu lagi untuk UserStorestatus, dll. Saya menemukan bahwa menghindari controller-views yang sangat bersarang mempertahankan titik masuk tunggal untuk data, dan menyatukan aliran data. Jika tidak, saya memiliki banyak sumber data, dan ini bisa menjadi sulit untuk di-debug.

Pengecekan jenis lebih sulit dengan strategi ini, tetapi Anda dapat menyiapkan "bentuk", atau jenis template, untuk large-object-as-prop dengan PropTypes dari React. Lihat: https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91 http://facebook.github.io/react/docs/reusable-components.html#prop -validasi

Perhatikan bahwa Anda mungkin ingin meletakkan logika data asosiasi antara penyimpanan di penyimpanan itu sendiri. Jadi Anda ArticleStorekekuatan waitFor()yang UserStore, dan termasuk Pengguna yang relevan dengan setiap Articlerecord menyediakan melalui getArticles(). Melakukan ini dalam pandangan Anda terdengar seperti mendorong logika ke dalam lapisan tampilan, yang merupakan praktik yang harus Anda hindari jika memungkinkan.

Anda mungkin juga tergoda untuk menggunakan transferPropsTo(), dan banyak orang suka melakukan ini, tetapi saya lebih suka menjaga agar semuanya tetap eksplisit agar mudah dibaca dan karenanya dapat dipelihara.

FWIW, pemahaman saya adalah bahwa David Nolen mengambil pendekatan serupa dengan kerangka kerja Om-nya (yang agak kompatibel dengan Flux ) dengan satu titik entri data pada simpul akar - padanan di Flux adalah hanya memiliki satu tampilan pengontrol mendengarkan semua toko. Ini dibuat efisien dengan menggunakan shouldComponentUpdate()dan struktur data yang tidak berubah yang dapat dibandingkan dengan referensi, dengan ===. Untuk struktur data yang tidak dapat diubah, periksa mori David atau -js Facebook yang tidak dapat diubah . Pengetahuan saya yang terbatas tentang Om terutama berasal dari The Future of JavaScript MVC Frameworks

Fisherwebdev
sumber
4
Terima kasih atas jawaban rinci! Saya benar-benar mempertimbangkan untuk meneruskan kedua snapshot seluruh status toko. Namun ini mungkin memberi saya masalah kinerja karena setiap pembaruan UserStore akan menyebabkan semua Artikel di layar dirender ulang, dan PureRenderMixin tidak akan dapat memberi tahu artikel mana yang perlu diperbarui, dan mana yang tidak. Adapun saran kedua Anda, saya memikirkannya juga, tetapi saya tidak dapat melihat bagaimana saya bisa menjaga persamaan referensi artikel jika pengguna artikel "diinjeksi" pada setiap panggilan getArticles (). Ini lagi-lagi berpotensi memberi saya masalah kinerja.
Dan Abramov
1
Saya mengerti maksud Anda, tetapi ada solusi. Pertama, Anda dapat memeriksa shouldComponentUpdate () jika larik ID pengguna untuk artikel telah berubah, yang pada dasarnya hanya menggulirkan fungsionalitas dan perilaku yang Anda inginkan di PureRenderMixin dalam kasus khusus ini.
Fisherwebdev
3
Benar, lagi pula saya akan hanya perlu melakukannya ketika saya yakin ada yang masalah kinerja yang seharusnya tidak menjadi kasus untuk toko yang update beberapa kali per menit max. Terima kasih lagi!
Dan Abramov
8
Bergantung pada jenis aplikasi yang Anda buat, mempertahankan satu titik masuk akan merugikan begitu Anda mulai memiliki lebih banyak "widget" di layar yang tidak secara langsung dimiliki oleh root yang sama. Jika Anda secara paksa mencoba melakukannya dengan cara itu, simpul akar tersebut akan memiliki tanggung jawab yang terlalu besar. KISS dan SOLID, meskipun itu dari sisi klien.
Rygu
37

Pendekatan di mana saya tiba adalah meminta setiap komponen menerima datanya (bukan ID) sebagai prop. Jika beberapa komponen bersarang membutuhkan entitas terkait, terserah komponen induk untuk mengambilnya.

Dalam contoh kami, Articleharus memiliki articleprop yang merupakan objek (mungkin diambil oleh ArticleListatau ArticlePage).

Karena Articlejuga ingin merender UserLinkdan UserAvataruntuk penulis artikel akan berlangganan UserStoredan mempertahankan author: UserStore.get(article.authorId)statusnya. Ini kemudian akan membuat UserLinkdan UserAvatardengan ini this.state.author. Jika mereka ingin menyebarkannya lebih jauh, mereka bisa. Tidak ada komponen turunan yang perlu mengambil pengguna ini lagi.

Untuk mengulangi:

  • Tidak ada komponen yang pernah menerima ID sebagai prop; semua komponen menerima objeknya masing-masing.
  • Jika komponen anak membutuhkan entitas, itu tanggung jawab orang tua untuk mengambilnya dan mengirimkannya sebagai prop.

Ini memecahkan masalah saya dengan cukup baik. Contoh kode yang ditulis ulang untuk menggunakan pendekatan ini:

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>
    )
  }
});

Ini membuat komponen terdalam tetap bodoh tetapi tidak memaksa kita untuk memperumit komponen tingkat atas.

Dan Abramov
sumber
Hanya untuk menambahkan perspektif di atas jawaban ini. Secara konseptual, Anda dapat mengembangkan gagasan kepemilikan, Mengingat Pengumpulan Data diselesaikan pada tampilan paling atas yang sebenarnya memiliki data (karenanya mendengarkan penyimpanannya). Beberapa Contoh: 1. AuthData harus dimiliki di App Component, setiap perubahan di Auth harus disebarluaskan sebagai props ke semua turunan oleh App Component. 2. ArticleData harus dimiliki oleh ArticlePage atau Article List seperti yang Disarankan dalam jawaban. sekarang setiap turunan ArticleList dapat menerima AuthData dan / atau ArticleData dari ArticleList terlepas dari komponen mana yang benar-benar mengambil data tersebut.
Saumitra R. Bhave
2

Solusi saya jauh lebih sederhana. Setiap komponen yang memiliki statusnya sendiri diperbolehkan untuk berbicara dan mendengarkan penyimpanan. Ini adalah komponen yang sangat mirip pengontrol. Komponen bersarang yang lebih dalam yang tidak mempertahankan status tetapi hanya merender barang tidak diizinkan. Mereka hanya menerima props untuk rendering murni, sangat mirip tampilan.

Dengan cara ini semuanya mengalir dari komponen stateful ke komponen stateless. Menjaga statefuls tetap rendah.

Dalam kasus Anda, Artikel akan menjadi stateful dan oleh karena itu berbicara ke toko dan UserLink dll hanya akan merender sehingga akan menerima article.user sebagai prop.

Rygu
sumber
1
Saya kira ini akan berfungsi jika artikel memiliki properti objek pengguna, tetapi dalam kasus saya hanya memiliki userId (karena pengguna sebenarnya disimpan di UserStore). Atau apakah saya kehilangan maksud Anda? Maukah Anda membagikan sebuah contoh?
Dan Abramov
Mulailah pengambilan untuk pengguna di artikel. Atau ubah backend API Anda untuk membuat id pengguna "dapat diperluas" sehingga Anda mendapatkan pengguna sekaligus. Dengan cara apa pun, pertahankan tampilan sederhana komponen bersarang yang lebih dalam dan andalkan hanya pada props.
Rygu
2
Saya tidak mampu memulai pengambilan dalam artikel karena harus dirender bersama. Sedangkan untuk API yang dapat diperluas, kami dulu memilikinya, tetapi sangat bermasalah untuk diurai dari Toko. Saya bahkan menulis perpustakaan untuk meratakan tanggapan karena alasan ini.
Dan Abramov
0

Masalah yang dijelaskan dalam 2 filosofi Anda adalah umum untuk aplikasi satu halaman mana pun.

Mereka dibahas secara singkat dalam video ini: https://www.youtube.com/watch?v=IrgHurBjQbg dan Relay ( https://facebook.github.io/relay ) dikembangkan oleh Facebook untuk mengatasi pengorbanan yang Anda jelaskan.

Pendekatan Relai sangat sentris data. Ini adalah jawaban untuk pertanyaan "Bagaimana cara mendapatkan hanya data yang diperlukan untuk setiap komponen dalam tampilan ini dalam satu kueri ke server?" Dan pada saat yang sama, Relay memastikan bahwa Anda memiliki sedikit penggandaan di seluruh kode saat komponen digunakan dalam beberapa tampilan.

Jika Relay bukan merupakan pilihan , "Semua komponen entitas membaca datanya sendiri" tampaknya merupakan pendekatan yang lebih baik bagi saya untuk situasi yang Anda gambarkan. Saya pikir kesalahpahaman di Flux adalah apa itu toko. Konsep penyimpanan tidak ada menjadi tempat di mana model atau koleksi benda disimpan. Toko adalah tempat sementara tempat aplikasi Anda meletakkan data sebelum tampilan dirender. Alasan sebenarnya mereka ada adalah untuk memecahkan masalah ketergantungan di seluruh data yang ada di penyimpanan yang berbeda.

Apa yang Flux tidak tentukan adalah bagaimana sebuah toko berhubungan dengan konsep model dan kumpulan objek (a la Backbone). Dalam hal ini beberapa orang sebenarnya membuat toko fluks sebagai tempat untuk meletakkan koleksi objek dari tipe tertentu yang tidak disiram untuk sepanjang waktu pengguna membiarkan browser tetap terbuka tetapi, seperti yang saya pahami, fluks, itu bukan penyimpanan. seharusnya.

Solusinya adalah memiliki lapisan lain tempat Anda tempat entitas yang diperlukan untuk membuat tampilan Anda (dan kemungkinan lainnya) disimpan dan terus diperbarui. Jika Anda lapisan ini yang model dan koleksi abstrak, tidak masalah jika Anda subkomponen harus query lagi untuk mendapatkan datanya sendiri.

Chris Cinelli
sumber
1
Sejauh yang saya mengerti, pendekatan Dan menjaga berbagai jenis objek dan mereferensikannya melalui id memungkinkan kami menyimpan cache dari objek tersebut dan memperbaruinya hanya di satu tempat. Bagaimana ini bisa dicapai jika, katakanlah, Anda memiliki beberapa Artikel yang mereferensikan pengguna yang sama, dan kemudian nama pengguna diperbarui? Satu-satunya cara adalah memperbarui semua Artikel dengan data pengguna baru. Ini adalah masalah yang sama persis yang Anda dapatkan saat memilih antara dokumen vs db relasional untuk backend Anda. Apa pendapat Anda tentang itu? Terima kasih.
Alex Ponomarev