
Tentang apa ini?
Kita akan berbicara tentang beberapa (lima, lebih spesifik) metode, trik, pengorbanan berdarah kepada Dewa Perusahaan, yang tampaknya membantu kita menulis kode yang lebih ringkas dan ekspresif dalam Aplikasi Redux (dan NGRX!) Kami. Cara diganggu oleh keringat dan kopi. Tolong tendang dan kritik keras. Kami akan belajar kode lebih baik bersama.
Jujur, pada awalnya saya hanya ingin memberi tahu dunia tentang perpustakaan-mikro baru saya (35 baris kode!) Kelas aksi-fluks , tetapi melihat semakin banyak seruan bahwa Habr akan segera menjadi Twitter, dan sebagian besar Setuju dengan mereka, saya memutuskan untuk mencoba membuat bacaan yang agak lebih luas. Jadi, kami bertemu 5 cara untuk memutakhirkan aplikasi Redux Anda!
Plat boiler keluar
Pertimbangkan contoh khas tentang cara mengirim permintaan AJAX ke Redux. Mari kita bayangkan bahwa kita benar-benar membutuhkan daftar segel dari server.
import { createSelector } from 'reselect' const actionTypeCatsGetInit = 'CATS_GET_INIT' const actionTypeCatsGetSuccess = 'CATS_GET_SUCCESS' const actionTypeCatsGetError = 'CATS_GET_ERROR' const actionCatsGetInit = () => ({ type: actionTypeCatsGetInit }) const actionCatsGetSuccess = (payload) => ({ type: actionTypeCatsGetSuccess, payload, }) const actionCatsGetError = (error) => ({ type: actionTypeCatsGetError, payload: error, }) const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case actionTypeCatsGetInit: return { ...state, loading: true, } case actionCatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case actionCatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) const makeSelectorCatsError = () => createSelector( (state) => state.cats.error, (error) => error, )
Jika Anda tidak mengerti mengapa pabrik penyeleksi diperlukan di sini, Anda dapat membacanya di sini.
Saya tidak sengaja mempertimbangkan efek samping di sini. Ini adalah topik untuk artikel terpisah yang penuh dengan kemarahan remaja dan kritik terhadap ekosistem yang ada: D
Ada beberapa titik lemah dalam kode ini:
- Pabrik tindakan memiliki keunikan tersendiri, tetapi kami masih menggunakan jenis tindakan.
- Saat entitas baru ditambahkan, kami terus menduplikasi logika yang sama untuk mengatur bendera
loading
. Data yang kami simpan dalam data
, dan formulirnya dapat bervariasi secara signifikan dari permintaan ke permintaan, tetapi indikator unduhan ( loading
bendera) akan tetap sama. - Switch run time adalah O (n) (well, hampir ). Ini sendiri bukan argumen yang sangat kuat, karena Redux, pada prinsipnya, bukan tentang kinerja. Itu membuat saya lebih marah bahwa untuk setiap
case
Anda perlu menulis beberapa baris tambahan kode penyajian, dan bahwa satu switch
tidak dapat dengan mudah dan indah dibagi menjadi beberapa. - Apakah kita benar-benar perlu menyimpan status kesalahan untuk setiap entitas secara terpisah?
- Selektor keren. Selektor memo sangat keren. Mereka memberi kita abstraksi di pihak kita, sehingga nantinya kita tidak perlu mengulang setengah aplikasi ketika mengubah formulirnya. Kami hanya mengubah pemilih itu sendiri. Apa yang tidak enak dipandang adalah serangkaian pabrik primitif yang dibutuhkan hanya karena kekhasan memoisasi dalam pemilihan .
Metode 1: Singkirkan Jenis Tindakan
Yah, tidak juga. Kami hanya membuat JS membuatnya untuk kami.
Mari kita berpikir sejenak tentang mengapa kita umumnya membutuhkan jenis tindakan. Yah, tentu saja, untuk memulai cabang logika yang diinginkan di peredam kami dan mengubah keadaan aplikasi yang sesuai. Pertanyaan sebenarnya adalah, apakah suatu tipe harus berupa string? Tetapi bagaimana jika kita menggunakan kelas dan apakah switch
berdasarkan jenis?
class CatsGetInit {} class CatsGetSuccess { constructor(responseData) { this.payload = responseData } } class CatsGetError { constructor(error) { this.payload = error this.error = true } } const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.constructor) { case CatsGetInit: return { ...state, loading: true, } case CatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case CatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } }
Segalanya tampak hebat, tetapi ada satu masalah: kami kehilangan serialisasi tindakan kami. Ini bukan lagi objek sederhana yang dapat kita konversi menjadi string dan sebaliknya. Sekarang kita mengandalkan fakta bahwa setiap aksi memiliki prototipe uniknya sendiri, yang, pada kenyataannya, memungkinkan desain seperti switch
pada tindakan. Anda tahu, saya benar-benar menyukai gagasan membuat serial tindakan saya menjadi string dan mengirimkannya bersama dengan laporan bug, dan saya tidak siap untuk menolaknya.
Jadi, setiap tindakan harus memiliki bidang type
(di sini Anda dapat melihat apa lagi yang harus dimiliki setiap tindakan yang menghormati tindakan). Untungnya, setiap kelas memiliki nama yang seperti string. Mari kita tambahkan type
pengambil type
setiap kelas yang akan mengembalikan nama kelas ini.
class CatsGetInit { constructor() { this.type = this.constructor.name } } const reducerCats = (state, action) => { switch (action.type) { case CatsGetInit.name: return { ...state, loading: true, }
Ini bahkan berfungsi, tetapi saya ingin menempelkan awalan untuk setiap jenis, seperti yang disarankan oleh Eric di bebek-modular-redux (saya sarankan melihat garpu re-bebek , yang bahkan lebih dingin, seperti untuk saya). Untuk menambahkan awalan, kita harus berhenti menggunakan nama kelas secara langsung, dan menambahkan pengambil lain. Sekarang statis.
class CatsGetInit { get static type () { return `prefix/${this.name}` } constructor () { this.type = this.constructor.type } } const reducerCats = (state, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, }
Mari kita sisir semua ini sedikit. Kurangi salin-tempel seminimal mungkin dan tambahkan kondisi lain: jika tindakan menampilkan kesalahan, maka payload
-nya harus bertipe Error
.
class ActionStandard { get static type () { return `prefix/${this.name}` } constructor(payload) { this.type = this.constructor.type this.payload = payload this.error = payload instanceof Error } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } case CatsGetSuccess.type: return { error: undefined, data: action.payload, loading: false, } case CatsGetError.type: return { ...data, error: action.payload, loading: false, } default: return state } }
Pada tahap ini, kode ini berfungsi baik dengan NGRX, tetapi Redux tidak mampu mengunyahnya. Dia bersumpah bahwa tindakan harus menjadi benda sederhana. Untungnya, JS memungkinkan kami mengembalikan hampir semua hal dari perancang, tetapi kami benar-benar tidak benar-benar membutuhkan rantai prototipe setelah membuat aksinya.
class ActionStandard { get static type () { return `prefix/${this.name}` } constructor(payload) { return { type: this.constructor.type, payload, error: payload instanceof Error } } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } case CatsGetSuccess.type: return { error: undefined, data: action.payload, loading: false, } case CatsGetError.type: return { ...data, error: action.payload, loading: false, } default: return state } }
Berdasarkan pertimbangan di atas, perpustakaan mikro fluks-action-class ditulis. Ada tes, cakupan uji 100% dan kelas ActionStandard
hampir sama dengan generik untuk kebutuhan TypeScript. Bekerja dengan TypeScript dan JavaScript.
Metode 2: Kami Tidak Takut Menggunakan CombineReducers
Idenya adalah sederhana untuk memalukan: gunakan CombedReducers tidak hanya untuk reducers tingkat atas, tetapi juga untuk lebih memecah logika dan membuat peredam terpisah untuk loading
.
const reducerLoading = (actionInit, actionSuccess, actionError) => ( state = false, action, ) => { switch (action.type) { case actionInit.type: return true case actionSuccess.type: return false case actionError.type: return false } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsData = (state = undefined, action) => { switch (action.type) { case CatsGetSuccess.type: return action.payload default: return state } } const reducerCatsError = (state = undefined, action) => { switch (action.type) { case CatsGetError.type: return action.payload default: return state } } const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerLoading(CatsGetInit, CatsGetSuccess, CatsGetError), error: reducerCatsError, })
Metode 3: Singkirkan saklar
Dan sekali lagi ide yang sangat sederhana: alih-alih switch-case
gunakan objek dari mana untuk memilih bidang yang diinginkan dengan kunci. Akses ke bidang objek dengan kunci adalah O (1), dan itu terlihat sedikit lebih bersih menurut pendapat saya.
const createReducer = (initialState, reducerMap) => ( state = initialState, action, ) => {
Mari kita refactor reducerLoading
. Sekarang, mengetahui tentang peta (objek) untuk reduksi, kita dapat mengembalikan peta ini dari reducerLoading
, alih-alih mengembalikan seluruh reducer. Berpotensi, ini membuka ruang lingkup tak terbatas untuk memperluas fungsionalitas.
const createReducer = (initialState, reducerMap) => ( state = initialState, action, ) => {
Dokumentasi resmi tentang Redux juga berbicara tentang pendekatan ini , namun, untuk beberapa alasan yang tidak diketahui, saya terus melihat banyak proyek menggunakan switch-case
. Berdasarkan kode dari dokumentasi resmi, Tn. Moshe telah menyusun perpustakaan untuk kami buat createReducer
.
Metode 4: Gunakan penangan kesalahan global
Kami benar-benar tidak perlu menyimpan kesalahan untuk setiap entitas secara terpisah. Dalam kebanyakan kasus, kami hanya ingin menunjukkan dialog. Dialog yang sama dengan teks dinamis untuk semua entitas.
Buat penangan kesalahan global. Dalam kasus paling sederhana, mungkin terlihat seperti ini:
class GlobalErrorInit extends ActionStandard {} class GlobalErrorClear extends ActionStandard {} const reducerError = createReducer(undefined, { [GlobalErrorInit.type]: (state, action) => action.payload, [GlobalErrorClear.type]: (state, action) => undefined, })
Kemudian di efek samping kami, kami akan mengirimkan tindakan ErrorInit
di catch
. Mungkin terlihat seperti ini saat menggunakan redux-thunk :
const catsGetAsync = async (dispatch) => { dispatch(new CatsGetInit()) try { const res = await fetch('https://cats.com/api/v1/cats') const body = await res.json() dispatch(new CatsGetSuccess(body)) } catch (error) { dispatch(new CatsGetError(error)) dispatch(new GlobalErrorInit(error)) } }
Sekarang kita bisa menyingkirkan bidang error
di toko cat kami dan menggunakan CatsGetError
hanya untuk mengganti bendera loading
.
class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading) })
Metode 5: Berpikir Sebelum Memoizing
Mari kita lihat tumpukan pabrik untuk penyeleksi lagi.
Saya melempar makeSelectorCatsError
karena tidak lagi diperlukan, seperti yang kita temukan di bab sebelumnya.
const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, )
Mengapa kami membutuhkan penyeleksi memo di sini? Apa sebenarnya yang kita coba untuk memo? Akses ke bidang objek dengan kunci, yang terjadi di sini, adalah O (1). Kita dapat menggunakan fungsi non-memo biasa. Gunakan memoisasi hanya ketika Anda ingin mengubah data dari toko sebelum memberikannya ke komponen.
const selectorCatsData = (state) => state.cats.data const selectorCatsLoading = (state) => state.cats.loading
Memoisasi masuk akal dalam hal menghitung hasil dengan cepat. Untuk contoh di bawah ini, mari kita bayangkan bahwa setiap kucing adalah objek dengan bidang name
, dan kami ingin mendapatkan string yang berisi nama-nama semua kucing.
const makeSelectorCatNames = () => createSelector( (state) => state.cats.data, (cats) => cats.data.reduce((accum, { name }) => `${accum} ${name}`, ''), )
Kesimpulan
Mari kita lihat lagi di mana kita mulai:
import { createSelector } from 'reselect' const actionTypeCatsGetInit = 'CATS_GET_INIT' const actionTypeCatsGetSuccess = 'CATS_GET_SUCCESS' const actionTypeCatsGetError = 'CATS_GET_ERROR' const actionCatsGetInit = () => ({ type: actionTypeCatsGetInit }) const actionCatsGetSuccess = () => ({ type: actionTypeCatsGetSuccess }) const actionCatsGetError = () => ({ type: actionTypeCatsGetError }) const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case actionTypeCatsGetInit: return { ...state, loading: true, } case actionCatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case actionCatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) const makeSelectorCatsError = () => createSelector( (state) => state.cats.error, (error) => error, )
Dan apa yang terjadi:
class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading) }) const selectorCatsData = (state) => state.cats.data const selectorCatsLoading = (state) => state.cats.loading
Saya harap Anda tidak membuang waktu dengan sia-sia, dan artikel itu setidaknya sedikit bermanfaat bagi Anda. Seperti yang saya katakan di awal, tolong tendang dan kritik keras. Kami akan belajar kode lebih baik bersama.