Segera spoiler kecil - mengatur keadaan dalam mobx tidak berbeda dengan mengatur keadaan umum tanpa menggunakan mobx dalam reaksi murni. Jawaban atas pertanyaan alami adalah mengapa, pada kenyataannya, apakah ini diperlukan? Anda akan menemukannya di akhir artikel. Sementara itu, artikel tersebut akan fokus pada organisasi negara dalam aplikasi reaksi bersih tanpa perpustakaan eksternal.

Reaksi menyediakan cara untuk menyimpan dan memperbarui status komponen menggunakan properti state pada instance komponen kelas dan metode setState. Namun demikian, di antara komunitas yang bereaksi, sekelompok perpustakaan tambahan dan pendekatan untuk bekerja dengan negara digunakan (fluks, redux, reduxasi, efektor, mobx, otak, banyak dari mereka). Tetapi apakah mungkin untuk membangun aplikasi yang cukup besar dengan sekelompok logika bisnis dengan sejumlah besar entitas dan hubungan data yang kompleks antara komponen yang hanya menggunakan setState? Apakah ada kebutuhan untuk perpustakaan tambahan untuk bekerja dengan negara? Mari kita cari tahu.
Jadi kami telah setState dan yang memperbarui negara dan memanggil renderer komponen. Tetapi bagaimana jika data yang sama diperlukan oleh banyak komponen yang tidak saling berhubungan? Di dok resmi reaksi terdapat bagian "mengangkat keadaan" dengan uraian terperinci - kami hanya menaikkan status ke leluhur yang sama dengan komponen-komponen ini, melewati alat peraga (dan melalui komponen perantara, jika perlu) data dan fungsi untuk mengubahnya. Untuk contoh kecil, ini terlihat masuk akal, tetapi kenyataannya adalah bahwa dalam aplikasi yang kompleks dapat ada banyak ketergantungan antara komponen dan kecenderungan untuk mentransfer status ke komponen umum leluhur mengarah pada kenyataan bahwa seluruh negara akan lebih tinggi dan lebih tinggi dan akan berakhir di komponen root dari App bersama dengan logika untuk memperbarui keadaan ini untuk semua komponen. Akibatnya, setState hanya akan terjadi untuk memperbarui komponen data lokal atau dalam komponen root dari App, di mana semua logika akan terkonsentrasi.
Tetapi apakah mungkin untuk menyimpan proses dan membuat status dalam aplikasi reaksi tanpa menggunakan setState atau pustaka tambahan dan menyediakan akses umum ke data ini dari komponen apa pun?
Objek javascript yang paling umum dan aturan tertentu untuk mengaturnya datang untuk membantu kami.
Tetapi pertama-tama Anda perlu mempelajari cara menguraikan aplikasi menjadi tipe entitas dan hubungannya.
Untuk memulainya, kami memperkenalkan objek yang akan menyimpan data global yang berlaku untuk seluruh aplikasi secara keseluruhan - (ini bisa menjadi pengaturan untuk gaya, lokalisasi, ukuran jendela, dll.) Dalam satu objek AppState dan hanya menempatkan objek ini ke dalam file terpisah.
// src/stores/AppState.js export const AppState = { locale: "en", theme: "...", .... }
Sekarang dalam komponen apa pun Anda dapat mengimpor dan menggunakan data toko kami.
import AppState from "../stores/AppState.js" const SomeComponent = ()=> ( <div> {AppState.locale === "..." ? ... : ...} </div> )
Kami melangkah lebih jauh - hampir setiap aplikasi memiliki esensi dari pengguna saat ini (tidak masalah bagaimana itu dibuat atau berasal dari server, dll.), Sehingga objek tunggal pengguna kami juga akan berada dalam keadaan aplikasi. Itu juga dapat dipindahkan ke file terpisah dan juga diimpor, atau dapat langsung disimpan di dalam objek AppState. Dan sekarang yang utama - Anda perlu menentukan diagram entitas yang membentuk aplikasi. Dalam hal basis data, ini akan berupa tabel dengan hubungan satu-ke-banyak atau banyak-ke-banyak, dan seluruh rantai hubungan ini dimulai dari esensi utama pengguna. Nah, dalam kasus kami, objek pengguna hanya akan menyimpan array dari objek-entitas-toko lain, di mana setiap objek-toko, pada gilirannya, akan menyimpan array dari toko-entitas lain.
Berikut ini adalah contoh - ada logika bisnis yang dinyatakan sebagai "pengguna dapat membuat / mengedit / menghapus folder, proyek di setiap folder, di setiap proyek tugas dan di setiap tugas subtugas" (ternyata sesuatu seperti manajer tugas) dan akan terlihat dalam diagram keadaan sesuatu seperti ini:
export const AppStore = { locale: "en", theme: "...", currentUser: { name: "...", email: "" folders: [ { name: "folder1", projects: [ { name: "project1", tasks: [ { text: "task1", subtasks: [ {text: "subtask1"}, .... ] }, .... ] }, ..... ] }, ..... ] } }
Sekarang komponen root dari App hanya dapat mengimpor objek ini dan memberikan beberapa informasi tentang pengguna, dan kemudian dapat mentransfer objek pengguna ke komponen dasbor
.... <Dashboard user={appState.user}/> ....
dan dia dapat membuat daftar folder
... <div>{user.folders.map(folder=><Folder folder={folder}/>)}</div> ...
dan setiap komponen folder akan menampilkan daftar proyek
.... <div>{folder.projects.map(project=><Project project={project}/>)}</div> ....
dan setiap komponen proyek dapat membuat daftar tugas
.... <div>{project.tasks.map(task=><Task task={task}/>)}</div> ....
dan akhirnya, setiap komponen tugas dapat menyajikan daftar subtugas dengan meneruskan objek yang diinginkan ke komponen subtugas
.... <div>{task.subtask.map(subtask=><Subtask subtask={subtask}/>)}</div> ....
Secara alami, pada satu halaman tidak ada yang akan menampilkan semua tugas dari semua proyek dari semua folder, mereka akan dibagi oleh panel samping (misalnya, daftar folder), oleh halaman, dll. Tetapi struktur umum kira-kira sama - komponen induk membuat komponen tertanam melewati sebuah objek dengan alat peraga data. Poin penting harus dicatat - objek apa pun (misalnya, objek folder, proyek, tugas) tidak disimpan dalam keadaan komponen apa pun - komponen hanya menerimanya melalui alat peraga sebagai bagian dari objek yang lebih umum. Dan misalnya, ketika komponen proyek melewati objek tugas ( <div>{project.tasks.map(task=><Task task={task}/>)}</div>
) ke komponen turunan Tugas, karena fakta bahwa objek disimpan di dalam satu objek tunggal Anda selalu dapat mengubah objek tugas ini dari luar - misalnya, AppState.currentUser.folders [2] .projects [3] .tasks [4] .text = "tugas yang diedit" dan kemudian menyebabkan komponen root diperbarui (ReactDOM.render (<App />> ) dan dengan cara ini kita mendapatkan status aplikasi saat ini.
Misalkan lebih lanjut bahwa kita ingin membuat subtugas baru ketika mengklik tombol "+" di komponen Tugas. Semuanya sederhana
onClick = ()=>{ this.props.task.subtasks.push({text: ""}); updateDOM() }
karena komponen Tugas menerima sebagai alat bantu objek tugas dan objek ini tidak disimpan di dalam keadaannya tetapi merupakan bagian dari toko AppState global (yaitu, objek tugas disimpan di dalam array tugas dari objek proyek yang lebih umum, dan pada gilirannya merupakan bagian dari objek pengguna dan pengguna sudah disimpan di dalam AppState ) dan berkat konektivitas ini, setelah menambahkan objek tugas baru ke array subtasks, Anda dapat memanggil pembaruan komponen root dan dengan demikian memperbarui dan memperbarui rumah untuk semua perubahan data (di mana pun mereka terjadi) hanya dengan memanggil fungsi pembaruan ateDOM, yang pada gilirannya hanya memperbarui komponen root.
export function updateDOM(){ ReactDom.render(<App/>, rootElement); }
Dan tidak masalah data bagian AppState mana dan dari tempat apa yang kami ubah (misalnya, Anda dapat meneruskan objek folder melalui alat peraga melalui komponen Proyek dan Tugas perantara ke komponen Subtask, dan hanya dapat memperbarui nama folder (this.props.folder.name = "nama baru ") - karena komponen menerima data melalui alat peraga, memperbarui komponen root akan memperbarui semua komponen bersarang dan memperbarui seluruh aplikasi.
Sekarang mari kita coba menambahkan beberapa kenyamanan untuk bekerja dengan sisi. Dalam contoh di atas, Anda dapat melihat bahwa membuat objek entitas baru setiap kali (misalnya project.tasks.push({text: "", subtasks: [], ...})
jika objek memiliki banyak properti dengan parameter default, maka setiap kali untuk membuat daftar mereka dan Anda dapat membuat kesalahan dan melupakan sesuatu, dll. Hal pertama yang terlintas dalam pikiran adalah untuk menempatkan penciptaan objek ke fungsi di mana bidang default akan ditugaskan dan pada saat yang sama mendefinisikan ulang mereka dengan data baru
function createTask(data){ return { text: "", subtasks: [], ... //many default fields ...data } }
tetapi jika Anda melihat dari sisi lain, fungsi ini adalah konstruktor dari entitas tertentu dan kelas javascript sangat bagus untuk peran ini
class Task { text: ""; subtasks: []; constructor(data){ Object.assign(this, data) } }
dan kemudian membuat objek hanya akan membuat turunan kelas dengan kemampuan untuk menimpa beberapa bidang default
onAddTask = ()=>{ this.props.project.tasks.push(new Task({...}) }
Lebih lanjut, Anda dapat melihat bahwa dengan cara yang sama, membuat kelas untuk objek proyek, pengguna, subtugas, kami mendapatkan duplikasi kode di dalam konstruktor
constructor()
tetapi kita dapat mengambil keuntungan dari pewarisan dan menarik kode ini ke konstruktor kelas dasar.
class BaseStore { constructor(data){ Object.update(this, data); } }
Lebih lanjut, Anda akan melihat bahwa setiap kali kami memperbarui beberapa keadaan, kami mengubah bidang objek secara manual
user.firstName = "..."; user.lastName = "..."; updateDOM();
dan menjadi sulit untuk melacak, tawar-menawar dan memahami apa yang terjadi dalam komponen dan oleh karena itu ada kebutuhan untuk menentukan saluran umum di mana pembaruan data apa pun akan melalui dan kemudian kita dapat menambahkan logging dan segala macam fasilitas lainnya. Untuk melakukan ini, solusinya adalah membuat metode pembaruan di kelas yang akan mengambil objek sementara dengan data baru dan memperbarui sendiri dan menetapkan aturan bahwa objek dapat diperbarui hanya melalui metode pembaruan dan bukan dengan penugasan langsung
class Task { update(newData){ console.log("before update", this); Object.assign(this, data); console.log("after update", this); } }
Nah, agar tidak menduplikasi kode di setiap kelas, kami juga memindahkan metode pembaruan ini ke kelas dasar.
Sekarang Anda dapat melihat bahwa ketika kami memperbarui beberapa data, kami harus memanggil metode updateDOM () secara manual. Namun dimungkinkan untuk melakukan pembaruan ini secara otomatis setiap kali panggilan ke metode pembaruan ({...}) dari kelas dasar terjadi.
Ternyata kelas dasar akan terlihat seperti ini
class BaseStore { constructor(data){ Object.update(this, data); } update(data){ Object.update(this, data); ReactDOM.render(<App/>, rootElement) } }
Nah, agar selama panggilan berturut-turut dari metode pembaruan () tidak ada pembaruan yang tidak perlu, Anda dapat menunda memperbarui komponen ke siklus acara berikutnya
let TimerId = 0; class BaseStore { constructor(data){ Object.update(this, data); } update(data){ Object.update(this, data); if(TimerId === 0) { TimerId = setTimeout(()=>{ TimerId = 0; ReactDOM.render(<App/>, rootElement); }) } } }
Selanjutnya, Anda dapat secara bertahap meningkatkan fungsionalitas kelas dasar - misalnya, agar tidak harus mengirim permintaan secara manual ke server setiap kali, selain memperbarui status, Anda dapat mengirim permintaan ke metode pembaruan ({..}) di latar belakang. Anda dapat mengatur saluran pembaruan langsung untuk soket web dengan menambahkan akun dari setiap objek yang dibuat di peta hash global tanpa mengubah komponen dan bekerja dengan data dengan cara apa pun.
Masih banyak yang harus dilakukan, tetapi saya ingin menyebutkan satu topik yang menarik - sangat sering melewati objek dengan data ke komponen yang diperlukan (misalnya, ketika komponen proyek membuat komponen tugas -
<div>{project.tasks.map(task=><Task task={task}/>)}</div>
komponen tugas yang paling mungkin memerlukan beberapa informasi yang tidak disimpan langsung di dalam tugas tetapi terletak di objek induk.
Misalkan Anda ingin mewarnai semua tugas dalam warna yang disimpan dalam proyek dan umum untuk semua tugas. Untuk melakukan ini, selain alat peraga tugas, komponen proyek juga harus mengirimkan alat peraga proyeknya <Task task={task} project={this.props.project}/>
. Dan jika Anda tiba-tiba perlu mewarnai tugas dengan warna yang sama untuk semua tugas dalam satu folder, Anda harus mentransfer objek folder saat ini dari komponen Folder ke komponen Tugas dengan meneruskannya melalui komponen Proyek perantara.
Muncul ketergantungan rapuh bahwa komponen harus tahu apa yang diperlukan komponen bersarang. Selain itu, kemungkinan konteks reaksi, meskipun akan menyederhanakan transfer melalui komponen perantara, masih akan memerlukan deskripsi penyedia dan pengetahuan tentang data apa yang diperlukan untuk komponen anak.
Tetapi masalah utama adalah bahwa setiap kali Anda mengedit desain atau mengubah daftar harapan pelanggan ketika suatu komponen membutuhkan informasi baru, Anda harus mengubah komponen yang lebih tinggi baik untuk meneruskan alat peraga atau membuat penyedia konteks. Saya ingin komponen menerima melalui alat peraga sebuah objek dengan data untuk mengakses bagian aplikasi kita. Dan di sini, javascript sangat cocok (tidak seperti bahasa fungsional seperti elm atau pendekatan abadi seperti redux) - sehingga objek dapat menyimpan tautan melingkar satu sama lain. Dalam kasus ini, objek tugas harus memiliki bidang tugas.proyek dengan tautan ke objek proyek induk di mana disimpan, dan objek proyek pada gilirannya harus memiliki tautan ke objek folder, dll., Ke objek AppState root. Dengan demikian, komponen, tidak peduli seberapa dalam itu, selalu dapat melalui objek induk melalui tautan dan mendapatkan semua informasi yang diperlukan dan tidak perlu membuangnya melalui banyak komponen perantara. Oleh karena itu, kami memperkenalkan aturan - setiap kali membuat objek Anda perlu menambahkan tautan ke objek induk. Misalnya, sekarang membuat tugas baru akan terlihat seperti ini
... const {project} = this.props; const newTask = new Task({project: this.props.project}) this.props.project.tasks.push(newTask);
Lebih jauh, dengan peningkatan logika bisnis, Anda dapat melihat bahwa bollerplate dikaitkan dengan dukungan backlink (misalnya, menetapkan tautan ke objek induk saat membuat objek baru atau misalnya, saat mentransfer proyek dari satu folder ke folder lain, ia tidak hanya akan memerlukan memperbarui proyek.folder = properti baruFolder diri Anda dari array proyek folder sebelumnya dan menambahkan folder baru ke array proyek) mulai mengulangi dan juga dapat dipindahkan ke kelas dasar sehingga ketika Anda membuat objek itu cukup untuk menentukan induk - new Task({project: this.porps.project})
new Task({project: this.porps.project})
dan kelas dasar akan secara otomatis menambahkan objek baru ke array project.tasks
dan juga ketika mentransfer tugas ke proyek lain, cukup dengan memperbarui bidang task.update({project: newProject})
dan kelas dasar akan secara otomatis menghapus tugas dari berbagai tugas dari proyek sebelumnya dan ditambahkan ke yang baru. Tetapi ini sudah membutuhkan deklarasi hubungan (misalnya, dalam properti atau metode statis) sehingga kelas dasar tahu bidang mana yang harus diperbarui.
Kesimpulan
Dengan cara yang sederhana, hanya menggunakan objek-js, kami sampai pada kesimpulan bahwa Anda bisa mendapatkan semua kenyamanan bekerja dengan keadaan umum aplikasi tanpa memperkenalkan ke dalam aplikasi ketergantungan pada perpustakaan eksternal untuk bekerja dengan negara.
Pertanyaannya adalah, mengapa kita membutuhkan perpustakaan untuk mengelola negara dan, khususnya, mobx?
Faktanya adalah bahwa dalam pendekatan yang dijelaskan untuk organisasi keadaan umum, ketika menggunakan objek "vanilla" js (atau objek kelas) asli biasa ada satu kelemahan besar - ketika sebagian kecil dari negara atau bahkan satu bidang berubah, komponen akan diperbarui atau "dirender" yang tidak terhubung dengan cara apa pun dan tidak bergantung pada bagian negara ini.
Dan pada aplikasi besar dengan huruf tebal, ini akan menyebabkan rem karena reaksinya tidak punya waktu untuk secara rekursif membandingkan rumah virtual dari seluruh aplikasi, mengingat bahwa selain membandingkan setiap penyaji, pohon objek baru akan dihasilkan setiap kali menggambarkan tata letak dari semua komponen.
Tetapi masalah ini, meskipun penting, adalah murni teknis - ada perpustakaan yang mirip dengan reaksi dom vitual yang lebih baik mengoptimalkan renderer dan dapat meningkatkan batas komponen.
Ada teknik renovasi rumah yang lebih efektif daripada membuat pohon rumah virtual baru dan lulus perbandingan rekursif berikutnya dengan pohon sebelumnya.
Dan akhirnya, ada perpustakaan yang mencoba memecahkan masalah pembaruan lambat melalui pendekatan yang berbeda - yaitu, untuk melacak bagian negara mana yang terhubung ke komponen mana dan ketika mengubah beberapa data, menghitung dan memperbarui hanya komponen-komponen yang bergantung pada data ini dan tidak menyentuh komponen yang tersisa. Redux juga perpustakaan seperti itu, tetapi membutuhkan pendekatan yang sama sekali berbeda untuk organisasi negara. Tetapi pustaka mobx, sebaliknya, tidak membawa sesuatu yang baru dan kita bisa mendapatkan percepatan penyaji secara praktis tanpa mengubah apa pun dalam aplikasi - cukup tambahkan dekorator yang dapat @observable
ke bidang kelas dan dekorator @observable
ke komponen yang membuat bidang ini tetap ada untuk hanya memotong kode pembaruan yang tidak perlu untuk komponen root dalam metode pembaruan () dari kelas dasar kami dan kami akan mendapatkan aplikasi yang berfungsi penuh, tetapi sekarang mengubah bagian dari keadaan atau bahkan satu bidang akan memperbarui hanya komponen-komponen itu yang jatuh tempo ditandatangani (memutar metode dalam render ()) untuk bidang tertentu dari negara tertentu objek.