React, Redux, dan Immutable.js: Bahan untuk Aplikasi Web yang Efisien

Diterbitkan: 2022-03-11

React, Redux, dan Immutable.js saat ini merupakan salah satu library JavaScript paling populer dan dengan cepat menjadi pilihan pertama pengembang dalam hal pengembangan front-end. Dalam beberapa proyek React dan Redux yang telah saya kerjakan, saya menyadari bahwa banyak pengembang yang memulai dengan React tidak sepenuhnya memahami React dan bagaimana menulis kode yang efisien untuk memanfaatkan potensi penuhnya.

Dalam tutorial Immutable.js ini, kita akan membuat aplikasi sederhana menggunakan React dan Redux, dan mengidentifikasi beberapa penyalahgunaan React yang paling umum dan cara untuk menghindarinya.

Masalah Referensi Data

React adalah tentang kinerja. Itu dibangun dari bawah ke atas untuk menjadi sangat berkinerja, hanya merender ulang bagian minimal DOM untuk memenuhi perubahan data baru. Setiap aplikasi React sebagian besar harus terdiri dari komponen kecil sederhana (atau fungsi stateless). Mereka mudah untuk dipikirkan dan kebanyakan dari mereka dapat memiliki fungsi shouldComponentUpdate yang mengembalikan false .

 shouldComponentUpdate(nextProps, nextState) { return false; }

Dari segi kinerja, fungsi siklus hidup komponen yang paling penting adalah shouldComponentUpdate dan jika memungkinkan harus selalu mengembalikan false . Ini memastikan bahwa komponen ini tidak akan pernah merender ulang (kecuali render awal) secara efektif membuat aplikasi React terasa sangat cepat.

Jika bukan itu masalahnya, tujuan kami adalah membuat pemeriksaan kesetaraan murah dari properti/status lama vs properti/status baru dan melewatkan rendering ulang jika data tidak berubah.

Mari kita mundur sejenak dan meninjau bagaimana JavaScript melakukan pemeriksaan kesetaraan untuk tipe data yang berbeda.

Pemeriksaan kesetaraan untuk tipe data primitif seperti boolean , string dan integer sangat sederhana karena selalu dibandingkan dengan nilai sebenarnya:

 1 === 1 'string' === 'string' true === true

Di sisi lain, pemeriksaan kesetaraan untuk tipe kompleks seperti objek , array , dan fungsi sama sekali berbeda. Dua objek adalah sama jika mereka memiliki referensi yang sama (menunjuk ke objek yang sama dalam memori).

 const obj1 = { prop: 'someValue' }; const obj2 = { prop: 'someValue' }; console.log(obj1 === obj2); // false

Meskipun obj1 dan obj2 tampak sama, referensinya berbeda. Karena mereka berbeda, membandingkannya secara naif dalam fungsi shouldComponentUpdate akan menyebabkan komponen kita dirender ulang secara tidak perlu.

Hal penting yang perlu diperhatikan adalah bahwa data yang berasal dari reduksi Redux, jika tidak diatur dengan benar, akan selalu disajikan dengan referensi berbeda yang akan menyebabkan komponen dirender ulang setiap saat.

Ini adalah masalah inti dalam upaya kami untuk menghindari rendering ulang komponen.

Penanganan Referensi

Mari kita ambil contoh di mana kita memiliki objek yang sangat bersarang dan kita ingin membandingkannya dengan versi sebelumnya. Kami dapat mengulang secara rekursif melalui alat peraga objek bersarang dan membandingkan masing-masing, tetapi jelas itu akan sangat mahal dan tidak mungkin.

Itu membuat kita hanya memiliki satu solusi, dan itu adalah memeriksa referensi, tetapi masalah baru muncul dengan cepat:

  • Mempertahankan referensi jika tidak ada yang berubah
  • Mengubah referensi jika salah satu nilai prop objek/array bersarang berubah

Ini bukan tugas yang mudah jika kita ingin melakukannya dengan cara yang bagus, bersih, dan kinerja dioptimalkan. Facebook menyadari masalah ini sejak lama dan memanggil Immutable.js untuk menyelamatkannya.

 import { Map } from 'immutable'; // transform object into immutable map let obj1 = Map({ prop: 'someValue' }); const obj2 = obj1; console.log(obj1 === obj2); // true obj1 = obj1.set('prop', 'someValue'); // set same old value console.log(obj1 === obj2); // true | does not break reference because nothing has changed obj1 = obj1.set('prop', 'someNewValue'); // set new value console.log(obj1 === obj2); // false | breaks reference

Tak satu pun dari fungsi Immutable.js melakukan mutasi langsung pada data yang diberikan. Sebagai gantinya, data dikloning secara internal, dimutasi dan jika ada perubahan, referensi baru dikembalikan. Jika tidak, ia mengembalikan referensi awal. Referensi baru harus diset secara eksplisit, seperti obj1 = obj1.set(...); .

Contoh React, Redux, dan Immutable.js

Cara terbaik untuk mendemonstrasikan kekuatan library ini adalah dengan membuat aplikasi sederhana. Dan apa yang bisa lebih sederhana dari aplikasi todo?

Untuk singkatnya, dalam artikel ini, kita hanya akan membahas bagian-bagian aplikasi yang penting untuk konsep-konsep ini. Seluruh kode sumber kode aplikasi dapat ditemukan di GitHub.

Saat aplikasi dimulai, Anda akan melihat bahwa panggilan ke console.log ditempatkan dengan nyaman di area utama untuk menunjukkan dengan jelas jumlah DOM re-render, yang minimal.

Seperti aplikasi todo lainnya, kami ingin menampilkan daftar item todo. Ketika pengguna mengklik item todo, kami akan menandainya sebagai selesai. Kami juga membutuhkan bidang input kecil di atas untuk menambahkan todos baru dan di 3 filter bawah yang memungkinkan pengguna untuk beralih di antara:

  • Semua
  • Lengkap
  • Aktif

Peredam Redux

Semua data dalam aplikasi Redux berada di dalam satu objek penyimpanan dan kita dapat melihat reduksi sebagai cara mudah untuk membagi penyimpanan menjadi bagian-bagian yang lebih kecil yang lebih mudah untuk dipikirkan. Karena peredam juga merupakan fungsi, itu juga dapat dipecah menjadi bagian-bagian yang lebih kecil.

Peredam kami akan terdiri dari 2 bagian kecil:

  • daftar todo
  • filter aktif
 // reducers/todos.js import * as types from 'constants/ActionTypes'; // we can look at List/Map as immutable representation of JS Array/Object import { List, Map } from 'immutable'; import { combineReducers } from 'redux'; function todoList(state = List(), action) { // default state is empty List() switch (action.type) { case types.ADD_TODO: return state.push(Map({ // Every switch/case must always return either immutable id: action.id, // or primitive (like in activeFilter) state data text: action.text, // We let Immutable decide if data has changed or not isCompleted: false, })); // other cases... default: return state; } } function activeFilter(state = 'all', action) { switch (action.type) { case types.CHANGE_FILTER: return action.filter; // This is primitive data so there's no need to worry default: return state; } } // combineReducers combines reducers into a single object // it lets us create any number or combination of reducers to fit our case export default combineReducers({ activeFilter, todoList, });

Menghubungkan dengan Redux

Sekarang kita telah menyiapkan peredam Redux dengan data Immutable.js, mari hubungkan dengan komponen React untuk meneruskan data.

 // components/App.js import { connect } from 'react-redux'; // ….component code const mapStateToProps = state => ({ activeFilter: state.todos.activeFilter, todoList: state.todos.todoList, }); export default connect(mapStateToProps)(App);

Di dunia yang sempurna, koneksi harus dilakukan hanya pada komponen rute tingkat atas, mengekstrak data di mapStateToProps dan sisanya adalah dasar React passing props ke anak-anak. Pada aplikasi skala besar, cenderung sulit untuk melacak semua koneksi sehingga kami ingin meminimalkannya.

Sangat penting untuk dicatat bahwa state.todos adalah objek JavaScript biasa yang dikembalikan dari fungsi Redux combineReducers (todos menjadi nama peredam), tetapi state.todos.todoList adalah Daftar yang Tidak Dapat Diubah dan sangat penting untuk tetap berada dalam kondisi seperti itu. form sampai melewati pemeriksaan shouldComponentUpdate .

Menghindari Re-render Komponen

Sebelum kita menggali lebih dalam, penting untuk memahami jenis data apa yang harus disajikan ke komponen:

  • Jenis primitif dalam bentuk apa pun
  • Objek/array hanya dalam bentuk yang tidak dapat diubah

Memiliki tipe data ini memungkinkan kita untuk membandingkan props yang masuk ke dalam komponen React secara dangkal.

Contoh berikut menunjukkan cara membedakan props dengan cara yang paling sederhana:

 $ npm install react-pure-render
 import shallowEqual from 'react-pure-render/shallowEqual'; shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); }

Fungsi dangkalEqual akan memeriksa props/status diff hanya 1 level dalam. Ini bekerja sangat cepat dan bersinergi sempurna dengan data abadi kami. Harus menulis shouldComponentUpdate ini di setiap komponen akan sangat merepotkan, tetapi untungnya ada solusi sederhana.

Ekstrak shouldComponentUpdate ke dalam komponen khusus yang terpisah:

 // components/PureComponent.js import React from 'react'; import shallowEqual from 'react-pure-render/shallowEqual'; export default class PureComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); } }

Kemudian cukup perpanjang komponen apa pun di mana logika shouldComponentUpdate ini diinginkan:

 // components/Todo.js export default class Todo extends PureComponent { // Component code }

Ini adalah cara yang sangat bersih dan efisien untuk menghindari rendering ulang komponen dalam banyak kasus, dan kemudian jika aplikasi menjadi lebih kompleks dan tiba-tiba membutuhkan solusi khusus, itu dapat diubah dengan mudah.

Ada sedikit masalah saat menggunakan PureComponent saat melewatkan fungsi sebagai props. Karena React, dengan ES6 class , tidak secara otomatis mengikat ini ke fungsi, kita harus melakukannya secara manual. Kita dapat mencapai ini dengan melakukan salah satu dari yang berikut:

  • gunakan pengikatan fungsi panah ES6: <Component onClick={() => this.handleClick()} />
  • gunakan bind : <Component onClick={this.handleClick.bind(this)} />

Kedua pendekatan akan menyebabkan Komponen dirender ulang karena referensi yang berbeda telah diteruskan ke onClick setiap saat.

Untuk mengatasi masalah ini, kita dapat melakukan pre-bind fungsi dalam metode konstruktor seperti:

 constructor() { super(); this.handleClick = this.handleClick.bind(this); } // Then simply pass the function render() { return <Component onClick={this.handleClick} /> }

Jika Anda sering melakukan pra-pengikatan beberapa fungsi, kami dapat mengekspor dan menggunakan kembali fungsi pembantu kecil:

 // utils/bind-functions.js export default function bindFunctions(functions) { functions.forEach(f => this[f] = this[f].bind(this)); } // some component constructor() { super(); bindFunctions.call(this, ['handleClick']); // Second argument is array of function names }

Jika tidak ada solusi yang bekerja untuk Anda, Anda selalu dapat menulis kondisi shouldComponentUpdate secara manual.

Menangani Data yang Tidak Dapat Diubah di Dalam Komponen

Dengan penyiapan data yang tidak dapat diubah saat ini, rendering ulang telah dihindari dan kita dibiarkan dengan data yang tidak dapat diubah di dalam properti komponen. Ada beberapa cara untuk menggunakan data yang tidak dapat diubah ini, tetapi kesalahan yang paling umum adalah mengonversi data langsung menjadi JS biasa menggunakan fungsi toJS yang tidak dapat diubah.

Menggunakan toJS untuk secara mendalam mengubah data yang tidak dapat diubah menjadi JS biasa meniadakan seluruh tujuan menghindari rendering ulang karena seperti yang diharapkan, ini sangat lambat dan karenanya harus dihindari. Jadi bagaimana kita menangani data yang tidak dapat diubah?

Itu perlu digunakan apa adanya, itulah sebabnya Immutable API menyediakan berbagai macam fungsi, map dan get yang paling umum digunakan di dalam komponen React. struktur data todoList yang berasal dari Redux Reducer adalah array objek dalam bentuk yang tidak dapat diubah, setiap objek mewakili satu item todo:

 [{ id: 1, text: 'todo1', isCompleted: false, }, { id: 2, text: 'todo2', isCompleted: false, }]

Immutable.js API sangat mirip dengan JavaScript biasa, jadi kami akan menggunakan todoList seperti array objek lainnya. Fungsi peta terbukti paling baik dalam banyak kasus.

Di dalam panggilan balik peta kita mendapatkan todo , yang merupakan objek yang masih dalam bentuk yang tidak dapat diubah dan kita dapat dengan aman meneruskannya dalam komponen Todo .

 // components/TodoList.js render() { return ( // …. {todoList.map(todo => { return ( <Todo key={todo.get('id')} todo={todo}/> ); })} // …. ); }

Jika Anda berencana melakukan beberapa iterasi berantai pada data yang tidak dapat diubah seperti:

 myMap.filter(somePred).sort(someComp)

... maka sangat penting untuk terlebih dahulu mengubahnya menjadi Seq menggunakan toSeq dan setelah iterasi mengubahnya kembali ke bentuk yang diinginkan seperti:

 myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()

Karena Immutable.js tidak pernah secara langsung mengubah data yang diberikan, ia selalu perlu membuat salinan lain darinya, melakukan beberapa iterasi seperti ini bisa sangat mahal. Seq adalah urutan data yang tidak dapat diubah, artinya ia akan melakukan operasi sesedikit mungkin untuk melakukan tugasnya sambil melewatkan pembuatan salinan perantara. Seq dibangun untuk digunakan dengan cara ini.

Di dalam komponen Todo gunakan get atau getIn untuk mendapatkan props.

Cukup sederhana bukan?

Yah, apa yang saya sadari adalah bahwa sering kali itu bisa menjadi sangat tidak terbaca memiliki banyak get() dan terutama getIn() . Jadi saya memutuskan untuk menemukan titik manis antara kinerja dan keterbacaan dan setelah beberapa percobaan sederhana saya menemukan bahwa fungsi Immutable.js toObject dan toArray bekerja dengan sangat baik.

Fungsi-fungsi ini mengonversi secara dangkal (kedalaman 1 level) objek/array Immutable.js menjadi objek/array JavaScript biasa. Jika kita memiliki data apa pun yang tersimpan di dalam, mereka akan tetap dalam bentuk yang tidak dapat diubah yang siap untuk diteruskan ke komponen anak-anak dan itulah yang kita butuhkan.

Ini lebih lambat dari get() hanya dengan margin yang dapat diabaikan, tetapi terlihat jauh lebih bersih:

 // components/Todo.js render() { const { id, text, isCompleted } = this.props.todo.toObject(); // ….. }

Mari Kita Lihat Semuanya dalam Aksi

Jika Anda belum mengkloning kode dari GitHub, sekarang adalah waktu yang tepat untuk melakukannya:

 git clone https://github.com/rogic89/ToDo-react-redux-immutable.git cd ToDo-react-redux-immutable

Memulai server sesederhana (pastikan Node.js dan NPM diinstal) seperti ini:

 npm install npm start 

Contoh Immutable.js: Aplikasi Todo, dengan bidang "masukkan todo", lima todo (kedua dan keempat dicoret), pemilih radio untuk semua vs. selesai vs. aktif, dan tombol "hapus semua".

Arahkan ke http://localhost:3000 di browser web Anda. Dengan konsol pengembang terbuka, perhatikan log saat Anda menambahkan beberapa item yang harus dilakukan, tandai sebagai selesai dan ubah filter:

  • Tambahkan 5 item todo
  • Ubah filter dari 'Semua' menjadi 'Aktif' lalu kembali ke 'Semua'
    • Tidak perlu melakukan rendering ulang, cukup filter perubahan
  • Tandai 2 item todo sebagai selesai
    • Dua todos dirender ulang, tetapi hanya satu per satu
  • Ubah filter dari 'Semua' menjadi 'Aktif' lalu kembali ke 'Semua'
    • Hanya 2 item tugas yang telah selesai dipasang/dilepas
    • Yang aktif tidak dirender ulang
  • Hapus satu item todo dari tengah daftar
    • Hanya item todo yang dihapus yang terpengaruh, yang lain tidak dirender ulang

Bungkus

Sinergi React, Redux, dan Immutable.js, jika digunakan dengan benar, menawarkan beberapa solusi elegan untuk banyak masalah kinerja yang sering ditemui di aplikasi web besar.

Immutable.js memungkinkan kita untuk mendeteksi perubahan pada objek/array JavaScript tanpa menggunakan pemeriksaan kesetaraan yang tidak efisien, yang pada gilirannya memungkinkan React untuk menghindari operasi rendering ulang yang mahal saat tidak diperlukan. Ini berarti kinerja Immutable.js cenderung bagus di sebagian besar skenario.

Saya harap Anda menyukai artikel ini dan menganggapnya berguna untuk membangun solusi inovatif React dalam proyek masa depan Anda.

Terkait: Bagaimana Komponen Bereaksi Membuat Pengujian UI Mudah