Kekekalan dalam JavaScript menggunakan Redux
Diterbitkan: 2022-03-11Dalam ekosistem aplikasi JavaScript yang kaya dan rumit yang terus berkembang, ada lebih banyak status yang harus dikelola daripada sebelumnya: pengguna saat ini, daftar posting yang dimuat, dll.
Kumpulan data apa pun yang membutuhkan riwayat peristiwa dapat dianggap stateful. Mengelola status bisa jadi sulit dan rawan kesalahan, tetapi bekerja dengan data yang tidak dapat diubah (bukan yang dapat diubah) dan teknologi pendukung tertentu - yaitu Redux, untuk tujuan artikel ini - dapat membantu secara signifikan.
Data yang tidak dapat diubah memiliki batasan, yaitu tidak dapat diubah setelah dibuat, tetapi juga memiliki banyak manfaat, terutama dalam referensi versus kesetaraan nilai, yang dapat sangat mempercepat aplikasi yang mengandalkan data yang sering dibandingkan (memeriksa apakah ada sesuatu yang perlu diperbarui , Misalnya).
Menggunakan status yang tidak dapat diubah memungkinkan kita untuk menulis kode yang dapat dengan cepat mengetahui apakah status telah berubah, tanpa perlu melakukan perbandingan rekursif pada data, yang biasanya jauh lebih cepat.
Artikel ini akan membahas aplikasi praktis Redux saat mengelola status melalui pembuat tindakan, fungsi murni, reduksi tersusun, tindakan tidak murni dengan Redux-saga dan Redux Thunk dan, terakhir, penggunaan Redux dengan React. Yang mengatakan, ada banyak alternatif untuk Redux, seperti perpustakaan berbasis MobX, Relay, dan Flux.
Mengapa Redux?
Aspek kunci yang membedakan Redux dari sebagian besar wadah status lainnya seperti MobX, Relay, dan sebagian besar implementasi berbasis Flux lainnya adalah bahwa Redux memiliki satu status yang hanya dapat dimodifikasi melalui "tindakan" (objek JavaScript biasa), yang dikirim ke Toko redux. Sebagian besar penyimpanan data lain memiliki status yang terkandung dalam komponen React itu sendiri, memungkinkan Anda untuk memiliki banyak penyimpanan dan/atau menggunakan status yang dapat diubah.
Ini pada gilirannya menyebabkan peredam toko, fungsi murni yang beroperasi pada data yang tidak dapat diubah, untuk mengeksekusi dan berpotensi memperbarui status. Proses ini memberlakukan aliran data searah, yang lebih mudah dipahami dan lebih deterministik.
Karena reduksi Redux adalah fungsi murni yang beroperasi pada data yang tidak dapat diubah, reduksi selalu menghasilkan output yang sama dengan input yang sama, sehingga mudah untuk diuji. Berikut ini contoh peredam:
import Immutable from 'seamless-immutable' const initialState = Immutable([]) // create immutable array via seamless-immutable /** * a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state. */ function addUserReducer(state = initialState, action) { if (action.type === 'USERS_ADD') { return state.concat(action.payload) } return state // note that a reducer MUST return a value } // somewhere else... store.dispatch({ type: 'USERS_ADD', payload: user }) // dispatch an action that causes the reducer to execute and add the user
Berurusan dengan fungsi murni memungkinkan Redux dengan mudah mendukung banyak kasus penggunaan yang umumnya tidak mudah dilakukan dengan status mutatif, seperti:
- Perjalanan waktu (Kembali ke masa lalu ke keadaan sebelumnya)
- Logging (Lacak setiap tindakan untuk mencari tahu apa yang menyebabkan mutasi di toko)
- Lingkungan kolaboratif (Seperti GoogleDocs, di mana tindakan adalah objek JavaScript biasa dan dapat diserialkan, dikirim melalui kabel, dan diputar ulang di komputer lain)
- Pelaporan bug yang mudah (Kirim saja daftar tindakan yang dikirim, dan putar ulang untuk mendapatkan status yang sama persis)
- Render yang dioptimalkan (Setidaknya dalam kerangka kerja yang membuat DOM virtual sebagai fungsi status, seperti React: karena kekekalan, Anda dapat dengan mudah mengetahui apakah ada sesuatu yang berubah dengan membandingkan referensi, bukan membandingkan objek secara rekursif)
- Uji reduksi Anda dengan mudah, karena fungsi murni dapat dengan mudah diuji unit
Pencipta Aksi
Pembuat tindakan Redux membantu menjaga kode tetap bersih dan dapat diuji. Ingat bahwa "tindakan" di Redux tidak lebih dari objek JavaScript biasa yang menjelaskan mutasi yang seharusnya terjadi. Meskipun demikian, menulis objek yang sama berulang-ulang adalah hal yang berulang dan rawan kesalahan.
Pembuat tindakan di Redux hanyalah fungsi pembantu yang mengembalikan objek JavaScript biasa yang menjelaskan mutasi. Ini membantu mengurangi kode berulang, dan menyimpan semua tindakan Anda di satu tempat:
export function usersFetched(users) { return { type: 'USERS_FETCHED', payload: users, } } export function usersFetchFailed(err) { return { type: 'USERS_FETCH_FAILED', payload: err, } } // reducer somewhere else... const initialState = Immutable([]) // create immutable array via seamless-immutable /** * a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state. */ function usersFetchedReducer(state = initialState, action) { if (action.type === 'USERS_FETCHED') { return Immutable(action.payload) } return state // note that a reducer MUST return a value }
Menggunakan Redux dengan Perpustakaan yang Tidak Dapat Diubah
Meskipun sifat dasar dari reduksi dan tindakan membuatnya mudah untuk diuji, tanpa pustaka pembantu yang tidak dapat diubah, tidak ada yang melindungi Anda dari objek yang bermutasi, yang berarti pengujian untuk semua reduksi Anda harus sangat kuat.
Pertimbangkan contoh kode berikut dari masalah yang akan Anda hadapi tanpa perpustakaan untuk melindungi Anda:
const initialState = [] function addUserReducer(state = initialState, action) { if (action.type === 'USERS_ADD') { state.push(action.payload) // NOTE: mutating action!! return state } return state // note that a reducer MUST return a value }
Dalam contoh kode ini, perjalanan waktu akan terputus karena keadaan sebelumnya sekarang akan sama dengan keadaan saat ini, komponen murni berpotensi tidak diperbarui (atau dirender ulang) karena referensi ke keadaan tidak berubah meskipun datanya berisi telah berubah, dan mutasi jauh lebih sulit untuk dipikirkan.
Tanpa perpustakaan kekekalan, kami kehilangan semua manfaat yang diberikan Redux. Oleh karena itu, sangat disarankan untuk menggunakan pustaka pembantu yang tidak dapat diubah, seperti immutable.js atau seamless-immutable, terutama saat bekerja dalam tim besar dengan banyak kode yang bersentuhan tangan.
Terlepas dari perpustakaan mana yang Anda gunakan, Redux akan berperilaku sama. Mari bandingkan pro dan kontra keduanya sehingga Anda dapat memilih mana yang paling cocok untuk kasus penggunaan Anda:
tidak berubah.js
Immutable.js adalah perpustakaan, dibangun oleh Facebook, dengan gaya yang lebih fungsional mengambil struktur data, seperti Peta, Daftar, Kumpulan, dan Urutan. Pustakanya dari struktur data persisten yang tidak dapat diubah melakukan jumlah penyalinan sesedikit mungkin di antara status yang berbeda.
Kelebihan:
- Berbagi struktural
- Lebih efisien dalam pembaruan
- Lebih hemat memori
- Memiliki serangkaian metode pembantu untuk mengelola pembaruan
Kontra:
- Tidak bekerja mulus dengan perpustakaan JS yang ada (yaitu lodash, ramda)
- Membutuhkan konversi ke dan dari (toJS / fromJS), terutama selama hidrasi / dehidrasi dan rendering
Mulus-tidak berubah
Seamless-immutable adalah perpustakaan untuk data yang tidak berubah yang kompatibel ke belakang hingga ES5.
Ini didasarkan pada fungsi definisi properti ES5, seperti defineProperty(..)
untuk menonaktifkan mutasi pada objek. Dengan demikian, ini sepenuhnya kompatibel dengan perpustakaan yang ada seperti lodash dan Ramda. Itu juga dapat dinonaktifkan di build produksi, memberikan peningkatan kinerja yang berpotensi signifikan.
Kelebihan:
- Bekerja mulus dengan perpustakaan JS yang ada (yaitu lodash, ramda)
- Tidak diperlukan kode tambahan untuk mendukung konversi
- Pemeriksaan dapat dinonaktifkan di build produksi, meningkatkan kinerja
Kontra:
- Tidak ada pembagian struktural - objek / larik disalin dengan dangkal, membuatnya lebih lambat untuk kumpulan data besar
- Tidak seefisien memori
Redux dan Multiple Reducer
Fitur lain yang berguna dari Redux adalah kemampuan untuk menyusun reduksi bersama-sama. Ini memungkinkan Anda untuk membuat aplikasi yang jauh lebih rumit, dan dalam aplikasi dengan ukuran apa pun, Anda pasti akan memiliki beberapa jenis status (pengguna saat ini, daftar posting yang dimuat, dll). Redux mendukung (dan mendorong) kasus penggunaan ini dengan secara alami menyediakan fungsi combineReducers
:
import { combineReducers } from 'redux' import currentUserReducer from './currentUserReducer' import postsListReducer from './postsListReducer' export default combineReducers({ currentUser: currentUserReducer, postsList: postsListReducer, })
Dengan kode di atas, Anda dapat memiliki komponen yang bergantung pada currentUser
dan komponen lain yang bergantung pada postsList
. Ini juga meningkatkan kinerja karena komponen tunggal mana pun hanya akan berlangganan ke cabang pohon apa pun yang menjadi perhatian mereka.
Tindakan Tidak Murni di Redux
Secara default, Anda hanya dapat mengirimkan objek JavaScript biasa ke Redux. Dengan middleware, bagaimanapun, Redux dapat mendukung tindakan tidak murni seperti mendapatkan waktu saat ini, melakukan permintaan jaringan, menulis file ke disk, dan sebagainya.
'Middleware' adalah istilah yang digunakan untuk fungsi yang dapat mencegat tindakan yang dikirim. Setelah dicegat, ia dapat melakukan hal-hal seperti mengubah tindakan atau mengirimkan tindakan asinkron, seperti middleware dalam kerangka kerja lain (seperti Express.js).
Dua perpustakaan middleware yang sangat umum adalah Redux Thunk dan Redux-saga. Redux Thunk ditulis dalam gaya imperatif, sedangkan Redux-saga ditulis dalam gaya fungsional. Mari kita bandingkan keduanya.

Redux Thunk
Redux Thunk mendukung tindakan tidak murni dalam Redux dengan menggunakan thunks, fungsi yang mengembalikan fungsi yang dapat dirantai lainnya. Untuk menggunakan Redux-Thunk, Anda harus terlebih dahulu memasang middleware Redux Thunk ke toko:
import { createStore, applyMiddleware } from 'redux' import thunk from 'redux-thunk' const store = createStore( myRootReducer, applyMiddleware(thunk), // here, we apply the thunk middleware to R )
Sekarang kita dapat melakukan tindakan tidak murni (seperti melakukan panggilan API) dengan mengirimkan thunk ke toko Redux:
store.dispatch( dispatch => { return api.fetchUsers() .then(users => dispatch(usersFetched(users)) // usersFetched is a function that returns a plain JavaScript object (Action) .catch(err => dispatch(usersFetchError(err)) // same with usersFetchError } )
Penting untuk dicatat bahwa menggunakan thunks dapat membuat kode Anda sulit untuk diuji dan mempersulit penalaran melalui aliran kode.
Redux-saga
Redux-saga mendukung tindakan tidak murni melalui fitur ES6 (ES2015) yang disebut generator dan perpustakaan pembantu fungsional / murni. Hal yang hebat tentang generator adalah mereka dapat dilanjutkan dan dijeda, dan kontrak API mereka membuatnya sangat mudah untuk diuji.
Mari kita lihat bagaimana kita dapat meningkatkan keterbacaan dan pengujian metode thunk sebelumnya menggunakan saga!
Pertama, mari pasang middleware Redux-saga ke toko kami:
import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import rootReducer from './rootReducer' import rootSaga from './rootSaga' // create the saga middleware const sagaMiddleware = createSagaMiddleware() // mount the middleware to the store const store = createStore( rootReducer, applyMiddleware(sagaMiddleware), ) // run our saga! sagaMiddleware.run(rootSaga)
Perhatikan bahwa fungsi run(..)
harus dipanggil dengan saga agar dapat mulai dieksekusi.
Sekarang mari kita buat saga kita:
import { call, put, takeEvery } from 'redux-saga/effects' // these are saga effects we'll use export function *fetchUsers(action) { try { const users = yield call(api.fetchUsers) yield put(usersFetched(users)) } catch (err) { yield put(usersFetchFailed(err)) } } export default function *rootSaga() { yield takeEvery('USERS_FETCH', fetchUsers) }
Kami mendefinisikan dua fungsi generator, satu yang mengambil daftar pengguna dan rootSaga
. Perhatikan bahwa kita tidak memanggil api.fetchUsers
secara langsung tetapi menghasilkannya dalam objek panggilan. Ini karena Redux-saga mencegat objek panggilan dan menjalankan fungsi yang terkandung di dalamnya untuk menciptakan lingkungan murni (sejauh menyangkut generator Anda).
rootSaga
menghasilkan satu panggilan ke fungsi yang disebut takeEvery,
yang mengambil setiap tindakan yang dikirim dengan tipe USERS_FETCH
dan memanggil kisah fetchUsers
dengan tindakan yang diambil. Seperti yang bisa kita lihat, ini menciptakan model efek samping yang sangat dapat diprediksi untuk Redux, yang membuatnya mudah untuk diuji!
Menguji Saga
Mari kita lihat bagaimana generator membuat kisah kita mudah untuk diuji. Kami akan menggunakan moka di bagian ini untuk menjalankan pengujian unit kami dan chai untuk pernyataan.
Karena saga menghasilkan objek JavaScript biasa dan dijalankan di dalam generator, kita dapat dengan mudah menguji apakah mereka melakukan perilaku yang benar tanpa olok-olok sama sekali! Ingatlah bahwa call
, take
, put
, dll hanyalah objek JavaScript biasa yang dicegat oleh middleware Redux-saga.
import { take, call } from 'redux-saga/effects' import { expect } from 'chai' import { rootSaga, fetchUsers } from '../rootSaga' describe('saga unit test', () => { it('should take every USERS_FETCH action', () => { const gen = rootSaga() // create our generator iterable expect(gen.next().value).to.be.eql(take('USERS_FETCH')) // assert the yield block does have the expected value expect(gen.next().done).to.be.equal(false) // assert that the generator loops infinitely }) it('should fetch the users if successful', () => { const gen = fetchUsers() expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded const users = [ user1, user2 ] // some mock response expect(gen.next(users).value).to.be.eql(put(usersFetched(users)) }) it('should fail if API fails', () => { const gen = fetchUsers() expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded const err = { message: 'authentication failed' } // some mock error expect(gen.throw(err).value).to.be.eql(put(usersFetchFailed(err)) }) })
Bekerja dengan React
Meskipun Redux tidak terikat pada pustaka pendamping tertentu, Redux bekerja sangat baik dengan React.js karena komponen React adalah fungsi murni yang mengambil status sebagai input dan menghasilkan DOM virtual sebagai output.
React-Redux adalah pustaka pembantu untuk React dan Redux yang menghilangkan sebagian besar kerja keras yang menghubungkan keduanya. Untuk menggunakan React-Redux secara paling efektif, mari kita bahas pengertian komponen presentasi dan komponen wadah.
Komponen presentasi menjelaskan bagaimana segala sesuatu harus terlihat secara visual, hanya bergantung pada properti yang akan dirender; mereka memanggil panggilan balik dari props ke tindakan pengiriman. Mereka ditulis dengan tangan, benar-benar murni, dan tidak terikat dengan sistem manajemen negara seperti Redux.
Komponen kontainer, di sisi lain, menjelaskan bagaimana segala sesuatunya harus berfungsi, menyadari Redux, mengirimkan tindakan Redux secara langsung untuk melakukan mutasi dan umumnya dihasilkan oleh React-Redux. Mereka sering dipasangkan dengan komponen presentasi, menyediakan alat peraganya.
Mari kita menulis komponen presentasi dan menghubungkannya ke Redux melalui React-Redux:
const HelloWorld = ({ count, onButtonClicked }) => ( <div> <span>Hello! You've clicked the button {count} times!</span> <button onClick={onButtonClicked}>Click me</button> </div> ) HelloWorld.propTypes = { count: PropTypes.number.isRequired, onButtonClicked: PropTypes.func.isRequired, }
Perhatikan bahwa ini adalah komponen "bodoh" yang sepenuhnya bergantung pada alat peraganya untuk berfungsi. Ini bagus, karena membuat komponen React mudah untuk diuji dan mudah dibuat. Mari kita lihat bagaimana menghubungkan komponen ini ke Redux sekarang, tetapi pertama-tama mari kita bahas apa itu Komponen Tingkat Tinggi.
Komponen Orde Tinggi
React-Redux menyediakan fungsi pembantu yang disebut connect( .. )
yang membuat komponen tingkat tinggi dari komponen React "bodoh" yang mengetahui Redux.
React menekankan ekstensibilitas dan kegunaan ulang melalui komposisi, yaitu ketika Anda membungkus komponen dalam komponen lain. Membungkus komponen ini dapat mengubah perilakunya atau menambahkan fungsionalitas baru. Mari kita lihat bagaimana kita dapat membuat komponen tingkat tinggi dari komponen presentasi kita yang mengetahui Redux - sebuah komponen kontainer.
Inilah cara Anda melakukannya:
import { connect } from 'react-redux' const mapStateToProps = state => { // state is the state of our store // return the props that we want to use for our component return { count: state.count, } } const mapDispatchToProps = dispatch => { // dispatch is our store dispatch function // return the props that we want to use for our component return { onButtonClicked: () => { dispatch({ type: 'BUTTON_CLICKED' }) }, } } // create our enhancer function const enhancer = connect(mapStateToProps, mapDispatchToProps) // wrap our "dumb" component with the enhancer const HelloWorldContainer = enhancer(HelloWorld) // and finally we export it export default HelloWorldContainer
Perhatikan bahwa kami mendefinisikan dua fungsi, mapStateToProps
dan mapDispatchToProps
.
mapStateToProps
adalah fungsi murni (status: Objek) yang mengembalikan objek yang dihitung dari status Redux. Objek ini akan digabungkan dengan props yang diteruskan ke komponen yang dibungkus. Ini juga dikenal sebagai pemilih, karena memilih bagian dari status Redux untuk digabungkan ke dalam prop komponen.
mapDispatchToProps
juga merupakan fungsi murni, tetapi salah satu dari (dispatch: (Action) => void) yang mengembalikan objek yang dihitung dari fungsi pengiriman Redux. Objek ini juga akan digabungkan dengan props yang diteruskan ke komponen yang dibungkus.
Sekarang untuk menggunakan komponen container, kita harus menggunakan komponen Provider
di React-Redux untuk memberi tahu komponen container penyimpanan apa yang akan digunakan:
import { Provider } from 'react-redux' import { render } from 'react-dom' import store from './store' // where ever your Redux store resides import HelloWorld from './HelloWorld' render( ( <Provider store={store}> <HelloWorld /> </Provider> ), document.getElementById('container') )
Komponen Provider
menyebarkan penyimpanan ke komponen anak mana pun yang berlangganan ke toko Redux, menyimpan semuanya di satu tempat dan mengurangi titik kesalahan atau mutasi!
Bangun Keyakinan Kode Dengan Redux
Dengan pengetahuan baru tentang Redux ini, banyak perpustakaan pendukungnya dan koneksi kerangka kerjanya dengan React.js, Anda dapat dengan mudah membatasi jumlah mutasi dalam aplikasi Anda melalui kontrol status. Kontrol status yang kuat, pada gilirannya, memungkinkan Anda bergerak lebih cepat dan membuat basis kode yang solid dengan lebih percaya diri.