Manajemen negara adalah salah satu tugas paling penting yang diselesaikan dalam pengembangan React. Banyak alat telah dibuat untuk membantu pengembang memecahkan masalah ini. Alat yang paling populer adalah Redux, perpustakaan kecil yang dibuat oleh Dan Abramov untuk membantu pengembang menggunakan pola desain Flux dalam aplikasi mereka. Di artikel ini, kita akan melihat apakah kita benar-benar membutuhkan Redux dan melihat bagaimana kita bisa menggantinya dengan pendekatan yang lebih sederhana berdasarkan Observable and React Hooks.
Mengapa kita membutuhkan Redux?
Redux sangat sering dikaitkan dengan Bereaksi sehingga banyak pengembang menggunakannya tanpa memikirkan mengapa mereka membutuhkan Redux. Bereaksi membuatnya mudah untuk menyinkronkan komponen dan useState()
dengan setState()
/ useState()
. Tetapi semuanya menjadi lebih rumit segera setelah negara mulai digunakan oleh beberapa komponen sekaligus. Solusi yang paling jelas untuk berbagi keadaan umum antara beberapa komponen adalah memindahkannya (keadaan) ke induknya yang sama. Tetapi solusi "langsung" seperti itu dapat dengan cepat menimbulkan kesulitan: jika komponen-komponen tersebut jauh dari satu sama lain dalam hierarki komponen, maka memperbarui keadaan umum akan membutuhkan banyak pengguliran melalui properti-properti komponen. Bereaksi Konteks dapat membantu mengurangi jumlah tumpahan, tetapi menyatakan konteks baru setiap kali negara mulai digunakan bersama dengan komponen lain akan membutuhkan lebih banyak upaya dan pada akhirnya dapat menyebabkan kesalahan.
Redux memecahkan masalah ini dengan memperkenalkan objek Store
yang berisi seluruh status aplikasi. Dalam komponen yang memerlukan akses untuk menyatakan, Store
ini diimplementasikan menggunakan fungsi connect
. Fungsi ini juga memastikan bahwa ketika keadaan berubah, semua komponen yang bergantung padanya akan digambar ulang. Akhirnya, untuk mengubah keadaan, komponen harus mengirim tindakan yang memicu peredam untuk menghitung keadaan yang baru diubah.

Ketika saya pertama kali memahami konsep Redux
Apa yang salah dengan Redux?
Pertama kali saya membaca tutorial resmi Redux , saya paling kaget dengan banyaknya kode yang harus saya tulis untuk mengubah status. Mengubah negara membutuhkan mendeklarasikan tindakan baru, mengimplementasikan peredam yang sesuai, dan akhirnya mengirimkan tindakan. Redux juga mendorong penulisan pembuat tindakan untuk memudahkan membuat tindakan setiap kali Anda ingin mengirimkannya.
Dengan semua langkah ini, Redux mempersulit pemahaman kode, refactoring, dan debugging. Saat membaca kode yang ditulis oleh orang lain, seringkali sulit untuk mengetahui apa yang terjadi ketika tindakan diajukan. Pertama, kita harus masuk ke kode pembuat tindakan untuk menemukan jenis tindakan yang sesuai, dan kemudian menemukan reduksi yang menangani jenis tindakan ini. Hal-hal dapat menjadi lebih rumit jika beberapa middlewares digunakan, seperti redux-saga, yang membuat konteks solusi lebih implisit.
Dan akhirnya, saat menggunakan TypeScript, Redux bisa mengecewakan. Secara desain, aksi hanyalah string yang terkait dengan parameter tambahan. Ada beberapa cara untuk menulis kode Redux yang diketik dengan baik menggunakan TypeScript, tetapi bisa sangat membosankan dan sekali lagi dapat menyebabkan peningkatan jumlah kode yang harus kita tulis.

Sensasi kode pembelajaran ditulis dengan Redux
Diamati dan kaitkan: pendekatan sederhana untuk mengelola negara.
Ganti Store dengan Observable
Untuk mengatasi masalah pembagian negara dengan cara yang lebih sederhana, pertama-tama kita perlu menemukan cara untuk memberi tahu komponen ketika komponen lain mengubah keadaan umumnya. Untuk melakukan ini, mari kita buat kelas yang dapat Observable
yang berisi nilai tunggal dan yang memungkinkan komponen untuk berlangganan perubahan nilai ini. Metode berlangganan akan mengembalikan fungsi yang harus dipanggil untuk berhenti berlangganan dari Observable
.
Di TypeScript, mengimplementasikan kelas semacam itu cukup sederhana:
type Listener<T> = (val: T) => void; type Unsubscriber = () => void; export class Observable<T> { private _listeners: Listener<T>[]; constructor(private _val: T) {} get(): T { return this._val; } set(val: T) { if (this._val !== val) { this._val = val; this._listeners.forEach(l => l(val)); } } subscribe(listener: Listener<T>): Unsubscriber { this._listeners.push(listener); return () => { this._listeners = this._listeners.filter(l => l !== listener); }; } }
Jika Anda membandingkan kelas ini dengan Redux Store
, Anda akan melihat bahwa mereka sangat mirip: get()
sesuai dengan getState()
, dan subscribe()
adalah sama. Perbedaan utama adalah metode dispatch()
, yang telah digantikan oleh metode set()
lebih sederhana, yang memungkinkan Anda untuk mengubah nilai yang terkandung di dalamnya tanpa harus bergantung pada peredam. Perbedaan signifikan lainnya adalah bahwa, berbeda dengan Redux, kami akan menggunakan banyak Observable
daripada satu Store
berisi semua status.
Ganti layanan peredam
Sekarang Observable
dapat digunakan untuk menyimpan keadaan umum, tetapi kita masih perlu memindahkan logika yang terkandung dalam peredam. Untuk ini kami menggunakan konsep layanan. Layanan adalah kelas yang mengimplementasikan seluruh logika bisnis aplikasi kita. Mari kita coba menulis ulang peredam Todo
dari tutorial Redux ke layanan Todo
menggunakan Observable
:
import { Observable } from "./observable"; export interface Todo { readonly text: string; readonly completed: boolean; } export enum VisibilityFilter { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE, } export class TodoService { readonly todos = new Observable<Todo[]>([]); readonly visibilityFilter = new Observable(VisibilityFilter.SHOW_ALL); addTodo(text: string) { this.todos.set([...this.todos.get(), { text, completed: false }]); } toggleTodo(index: number) { this.todos.set(this.todos.get().map( (todo, i) => (i === index ? { text: todo.text, completed: !todo.completed } : todo) )); } setVisibilityFilter(filter: VisibilityFilter) { this.visibilityFilter.set(filter); } }
Membandingkan ini dengan peredam Todo
, kita dapat mencatat perbedaan berikut:
- Tindakan digantikan oleh metode, menghilangkan kebutuhan untuk menyatakan jenis tindakan, tindakan itu sendiri, dan pembuat tindakan.
- Anda tidak perlu lagi menulis peralihan besar untuk merutekan di antara jenis tindakan. Pengiriman Javascript dinamis (mis. Panggilan metode) menangani hal ini.
- Dan yang paling penting, layanan berisi dan mengubah keadaan yang dikelolanya. Ini adalah perbedaan konseptual besar dari reduksi, yang merupakan fungsi murni.
Akses ke layanan dan dapat diamati dari komponen
Sekarang kami telah mengganti "store and reducer from Redux" dengan "Observable and services", kami perlu menyediakan layanan dari semua komponen Bereaksi. Ada beberapa cara untuk melakukan ini: kita bisa menggunakan kerangka kerja IoC, misalnya, Membalikkan; gunakan konteks Bereaksi atau gunakan pendekatan yang sama seperti di Store
Redux - satu contoh global untuk setiap layanan. Dalam artikel ini, kami akan mempertimbangkan pendekatan terakhir:
import { TodoService } from "./todoService"; export const todoService = new TodoService();
Sekarang kita dapat mengakses status bersama dan mengubahnya dari semua komponen Bereaksi kita dengan mengimpor turunan todoService
. Tetapi kita masih perlu menemukan cara untuk menggambar ulang komponen kita ketika keadaan umum diubah oleh komponen lain. Untuk melakukan ini, kita akan menulis sebuah kait sederhana yang menambahkan variabel status ke komponen, berlangganan Observable
dan memperbarui variabel state ketika nilai Observable
berubah:
import { useEffect, useState } from "react"; import { Observable } from "./observable"; export function useObservable<T>(observable: Observable<T>): T { const [val, setVal] = useState(observable.get()); useEffect(() => { setVal(observable.get());
Menyatukan semuanya
Toolkit kami siap. Kita dapat menggunakan Observable
untuk menyimpan status umum dalam layanan dan menggunakan useObservable
untuk memastikan bahwa komponen akan selalu selaras dengan kondisi ini.
Mari kita menulis ulang komponen TodoList dari tutorial Redux menggunakan hook baru:
import React from "react"; import { useObservable } from "./observableHook"; import { todoService } from "./services"; import { Todo, VisibilityFilter } from "./todoService"; export const TodoList = () => { const todos = useObservable(todoService.todos); const filter = useObservable(todoService.visibilityFilter); const visibleTodos = getVisibleTodos(todos, filter); return ( <div> <ul> {visibleTodos.map((todo, index) => ( <TodoItem key={index} todo={todo} index={index} /> ))} </ul> <p> Show: <FilterLink filter={VisibilityFilter.SHOW_ALL}>All</FilterLink>, <FilterLink filter={VisibilityFilter.SHOW_ACTIVE}>Active</FilterLink>, <FilterLink filter={VisibilityFilter.SHOW_ALL}>Completed</FilterLink> </p> </div> ); }; const TodoItem = ({ todo: { text, completed }, index }: { todo: Todo; index: number }) => { return ( <li style={{ textDecoration: completed ? "line-through" : "none", }} onClick={() => todoService.toggleTodo(index)} > {text} </li> ); }; const FilterLink = ({ filter, children }: { filter: VisibilityFilter; children: React.ReactNode }) => { const activeFilter = useObservable(todoService.visibilityFilter); const active = filter === activeFilter; return active ? ( <span>{children}</span> ) : ( <a href="" onClick={() => todoService.setVisibilityFilter(filter)}> {children} </a> ); }; function getVisibleTodos(todos: Todo[], filter: VisibilityFilter): Todo[] { switch (filter) { case VisibilityFilter.SHOW_ALL: return todos; case VisibilityFilter.SHOW_COMPLETED: return todos.filter(t => t.completed); case VisibilityFilter.SHOW_ACTIVE: return todos.filter(t => !t.completed); } }
Seperti yang dapat kita lihat, kita telah menulis beberapa komponen yang merujuk ke nilai keadaan umum ( todos
dan visibilityFilter
). Nilai-nilai ini hanya diubah dengan memanggil metode dari todoService
. Berkat hook useObservable, yang berlangganan perubahan nilai, komponen ini secara otomatis digambar ulang ketika keadaan umum berubah.
Kesimpulan
Jika kita membandingkan kode ini dengan pendekatan Redux, kita akan melihat beberapa keuntungan:
- Keringkasan: satu - satunya hal yang perlu kita lakukan adalah membungkus nilai-nilai keadaan di
Observable
dan menggunakan hook useObservable
ketika mengakses nilai-nilai ini dari komponen. Tidak perlu mendeklarasikan tindakan, pembuat tindakan, menulis atau menggabungkan peredam, atau menghubungkan komponen kami ke repositori dengan mapDispatchToProps
dan mapDispatchToProps
. - Kesederhanaan: Sekarang jauh lebih mudah untuk melacak eksekusi kode. Memahami apa yang sebenarnya terjadi ketika tombol ditekan hanyalah masalah beralih ke implementasi metode yang disebut. Eksekusi langkah demi langkah menggunakan debugger juga meningkat secara signifikan, karena tidak ada tingkat menengah antara komponen kami dan layanan kami.
- Ketik keamanan di luar kotak: Pengembang TypeScript tidak perlu pekerjaan tambahan untuk memiliki kode yang diketik dengan benar. Tidak perlu mendeklarasikan tipe untuk negara dan untuk setiap tindakan.
- Dukungan untuk async / menunggu: walaupun ini belum diperlihatkan di sini, solusi ini bekerja sangat baik dengan fungsi asinkron, sangat menyederhanakan pemrograman asinkron. Tidak perlu bergantung pada middleware, seperti redux-thunk, yang membutuhkan pengetahuan mendalam tentang pemrograman fungsional untuk memahami.
Redux, tentu saja, masih memiliki beberapa keuntungan serius, terutama Redux DevTools, yang memungkinkan pengembang untuk memantau perubahan status selama pengembangan dan bergerak dalam waktu ke kondisi aplikasi sebelumnya, yang dapat menjadi alat yang hebat untuk debugging. Tetapi dalam pengalaman saya, saya jarang menggunakan ini, dan harga yang harus dibayar tampaknya terlalu tinggi untuk keuntungan kecil.
Dalam semua aplikasi Bereaksi dan Bereaksi Asli kami, kami telah berhasil menggunakan pendekatan yang mirip dengan yang dijelaskan dalam artikel ini. Faktanya, kami tidak pernah merasakan kebutuhan akan sistem manajemen negara yang lebih kompleks daripada ini.
Catatan
Kelas yang dapat Observable
diperkenalkan dalam posting ini cukup sederhana. Ini dapat diganti dengan implementasi yang lebih maju seperti micro-observable (perpustakaan kami sendiri) atau RxJS.
Solusi yang disajikan di sini sangat mirip dengan apa yang dapat dicapai dengan MobX. Perbedaan utama adalah bahwa MobX mendukung sifat yang dapat berubah-ubah dari keadaan objek. Itu juga bergantung pada proksi ES6 untuk memberitahukan perubahan, secara implisit merender ulang, dan menyulitkan proses debug ketika hal-hal tidak berjalan seperti yang diharapkan. Selain itu, MobX tidak berfungsi dengan baik dengan fungsi asinkron.
Dari penerjemah: Publikasi ini, yang dapat dilihat sebagai pengantar manajemen negara menggunakan Observable, adalah kelanjutan dari topik yang dibahas dalam artikel Mengelola Aplikasi Negara dengan RxJS / Immer sebagai alternatif sederhana untuk Redux / MobX , yang menjelaskan cara menyederhanakan penggunaan pendekatan ini.