- Pengembangan pustaka komponen pada React + Storybook
- Test Driven Development di JS atau Cara Memulai Pemrograman yang Penuh Kasih
- Migrasi proyek nyata dari Javascript ke Typecript - rasa sakit dan fitur
Sekarang mari kita lanjutkan ke artikel.
Ketika saya mulai mempelajari React, ada beberapa hal yang tidak saya mengerti. Dan saya rasa hampir semua orang yang akrab dengan React menanyakan pertanyaan yang sama. Saya yakin akan hal ini karena orang-orang membangun seluruh perpustakaan untuk memecahkan masalah yang mendesak. Berikut adalah dua pertanyaan utama yang tampaknya dipedulikan oleh hampir setiap pengembang React:
Bagaimana satu komponen mengakses informasi (terutama variabel status) yang ada di komponen lain? Bagaimana satu komponen memanggil fungsi yang ada di komponen lain?
Pengembang JavaScript pada umumnya (dan pengembang React pada khususnya) semakin tertarik untuk menulis apa yang disebut fungsi murni belakangan ini. Fungsi yang tidak terkait dengan perubahan status. Fungsi yang tidak memerlukan koneksi database eksternal. Fungsi yang tidak bergantung pada apa yang terjadi di luarnya.
Tentu saja, fungsi murni adalah tujuan yang mulia. Tetapi jika Anda mengembangkan aplikasi yang lebih atau kurang kompleks, maka Anda tidak akan dapat membuat semua fungsi menjadi bersih. Pasti akan tiba saatnya ketika Anda harus membuat setidaknya beberapa komponen yang entah bagaimana terkait dengan komponen lain . Mencoba menghindari hal ini konyol. Hubungan antar komponen ini disebut dependensi .
Secara umum, dependensi itu buruk dan paling baik digunakan hanya jika diperlukan. Tetapi sekali lagi, jika aplikasi Anda telah berkembang, beberapa komponennya akan bergantung satu sama lain. Tentu saja, pengembang React mengetahui hal ini, jadi mereka menemukan cara untuk membuat satu komponen meneruskan informasi penting, atau fungsi, ke komponen turunannya.
Pendekatan standar: gunakan alat peraga untuk meneruskan nilai
Setiap nilai status dapat diteruskan ke komponen lain melalui props. Fungsi apa pun dapat diteruskan ke komponen anak melalui props yang sama. Beginilah cara keturunan mengetahui nilai status apa yang disimpan di pohon, dan berpotensi dapat memanggil tindakan di komponen induk. Semua ini, tentu saja, bagus. Tetapi pengembang React prihatin tentang masalah tertentu.
Sebagian besar aplikasi berlapis. Dalam aplikasi yang kompleks, struktur dapat disarangkan dengan sangat dalam. Arsitektur umum mungkin terlihat seperti ini:
App→ mengacu pada → ContentArea
ContentArea→ mengacu pada → MainContentArea
MainContentArea→ mengacu pada → MyDashboard
MyDashboard→ mengacu pada → MyOpenTickets
MyOpenTickets→ mengacu pada → TicketTable
TicketTable→ mengacu pada urutan → TicketRow
Setiap
TicketRow→ mengacu pada →TicketDetail
Secara teoritis, karangan bunga ini bisa dililitkan dalam waktu yang lama. Semua komponen adalah bagian dari keseluruhan. Lebih tepatnya, bagian dari hierarki. Tetapi di sini muncul pertanyaan:
Dapatkah komponen
TicketDetaildalam contoh di atas membaca nilai status yang disimpan ContentArea? Atau. Bisakah TicketDetailpanggilan komponen berfungsi ContentArea?
Jawaban untuk kedua pertanyaan tersebut adalah ya. Secara teori, semua turunan dapat mengetahui tentang semua variabel yang disimpan di komponen induk. Mereka juga dapat memanggil fungsi leluhur - tetapi dengan peringatan besar. Ini hanya mungkin jika nilai seperti itu (nilai status atau fungsi) secara eksplisit diteruskan ke turunan melalui props. Jika tidak, status komponen atau nilai fungsi tidak akan tersedia untuk komponen anaknya.
Dalam aplikasi dan utilitas kecil, ini tidak memainkan peran khusus. Misalnya, jika sebuah komponen
TicketDetailperlu mengakses variabel keadaan yang disimpan di TicketRowdalamnya, itu cukup untuk membuat komponen TicketRow→ meneruskan nilai-nilai ini ke turunannya → TicketDetailmelalui satu atau beberapa props. Hal yang sama terjadi ketika komponen TicketDetailperlu memanggil fungsi yang ada di dalamnya TicketRow. Komponen TicketRow→ akan meneruskan fungsi ini ke turunannya → TicketDetailmelalui prop. Sakit kepala dimulai ketika komponen yang jauh di bawah pohon perlu mengakses status atau fungsi komponen di bagian atas hierarki.
Untuk mengatasi masalah ini, React secara tradisional meneruskan variabel dan fungsi ke semua level. Tetapi ini mengacaukan kode, menghabiskan sumber daya dan membutuhkan perencanaan yang serius. Kita harus meneruskan nilai ke banyak level seperti ini:
ContentArea→ MainContentArea→ MyDashboard→ MyOpenTickets→ TicketTable→ TicketRow→ TicketDetail
Artinya, untuk meneruskan variabel status dari
ContentAreake TicketDetail, kita perlu melakukan banyak pekerjaan. Pengembang yang berpengalaman memahami bahwa ada rantai panjang yang jelek dari nilai-nilai dan fungsi yang lewat dalam bentuk alat peraga melalui komponen tingkat menengah. Solusinya sangat tidak praktis sehingga saya bahkan beberapa kali berhenti belajar React karenanya.
Monster itu bernama Redux
Saya bukan satu-satunya yang berpikir bahwa meneruskan semua nilai status dan semua fungsi yang umum ke komponen melalui props sangat tidak praktis. Anda tidak mungkin menemukan aplikasi React yang kompleks yang tidak dilengkapi dengan alat manajemen status. Tidak sedikit alat seperti itu. Secara pribadi, saya suka MobX. Sayangnya, Redux dianggap sebagai "standar industri".
Redux adalah gagasan dari pencipta inti React. Artinya, mereka pertama kali membuat perpustakaan React yang luar biasa. Tetapi mereka segera menyadari bahwa dengan dia berarti hampir tidak mungkin untuk mengelola negara. Jika mereka tidak menemukan cara untuk memecahkan masalah inheren dari perpustakaan (jika tidak hebat) ini, banyak dari kita tidak akan pernah mendengar tentang React.
Jadi mereka datang dengan Redux.
Jika React adalah Mona Lisa, maka Redux adalah kumis yang melekat padanya. Jika Anda menggunakan Redux, Anda harus menulis banyak kode boilerplate di hampir setiap file proyek. Memecahkan masalah dan membaca kode menjadi neraka. Logika bisnis dibawa ke halaman belakang. Kode mengandung kebingungan dan kebimbangan.
Tetapi jika pengembang memiliki pilihan: React + Redux atau React tanpa alat manajemen negara pihak ketiga , mereka hampir selalu memilih React + Redux. Karena pustaka Redux dikembangkan oleh penulis inti React, ini dianggap sebagai solusi yang disetujui secara default. Dan sebagian besar pengembang lebih suka menggunakan solusi yang telah disetujui secara diam-diam seperti ini.
Tentu saja Redux akan membuat seluruh web dependensidalam aplikasi React Anda. Namun, dalam keadilan, setiap alat manajemen negara umum akan melakukan hal yang sama. Alat manajemen negara adalah tempat penyimpanan variabel dan fungsi bersama. Fungsi dan variabel seperti itu dapat digunakan oleh komponen apa pun yang memiliki akses ke penyimpanan bersama. Ini memiliki satu kelemahan yang jelas: semua komponen menjadi bergantung pada penyimpanan bersama.
Sebagian besar pengembang React yang saya kenal yang mencoba menolak menggunakan Redux akhirnya menyerah. (Karena ... perlawanan tidak berguna.) Saya tahu banyak orang yang langsung membenci Redux. Tetapi ketika mereka dihadapkan pada pilihan - Redux atau "kami akan mencari pengembang React lain" - mereka melemparkan diri mereka sendiritelah setuju untuk menggunakan Redux sebagai bagian integral dari kehidupan mereka. Ini seperti pajak. Seperti pemeriksaan rektal. Seperti pergi ke dokter gigi.
Reacting Shared Values di React
Aku terlalu keras kepala untuk menyerah begitu saja. Setelah melihat Redux, saya menyadari bahwa saya perlu mencari solusi lain. Saya bisa menggunakan Redux. Dan saya bekerja dalam tim yang menggunakan perpustakaan ini. Secara umum, saya mengerti apa yang dia lakukan. Tapi itu tidak berarti saya suka Redux.
Seperti yang saya katakan, sementara alat manajemen keadaan terpisah sangat diperlukan, MobX sekitar ... satu juta kali lebih baik daripada Redux! Tapi saya tersiksa oleh pertanyaan yang lebih serius. Ini menyentuh pikiran kolektif pengembang React:
Mengapa kita selalu menggunakan alat manajemen negara terlebih dahulu?
Ketika saya pertama kali mulai mengembangkan dengan React, saya menghabiskan banyak malam mencari solusi alternatif. Dan saya menemukan cara yang diabaikan oleh banyak pengembang React, tetapi tidak satupun dari mereka yang tahu mengapa . Akan menjelaskan.
Bayangkan dalam aplikasi hipotetis yang saya tulis di atas, kami membuat file seperti ini:
// components.js
let components = {};
export default components;
Dan itu saja. Hanya dua baris kode pendek. Kami membuat objek kosong - objek JS lama yang bagus . Kami mengekspornya secara default dengan
export default.
Sekarang mari kita lihat seperti apa kode di dalam komponen itu
<ContentArea>:
// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';
export default class ContentArea extends React.Component {
constructor(props) {
super(props);
components.ContentArea = this;
}
consoleLog(value) {
console.log(value);
}
render() {
return <MainContentArea/>;
}
}
Untuk sebagian besar, ini terlihat seperti komponen React berbasis kelas yang normal. Kami memiliki fungsi sederhana
render()yang mengakses komponen berikutnya di bawah pohon. Kami memiliki fungsi kecil console.log()yang mencetak hasil eksekusi kode ke konsol, dan konstruktor. Tapi ... ada beberapa nuansa dalam konstruktornya .
Pada awalnya, kami mengimpor objek sederhana
components. Kemudian, di konstruktor, kami menambahkan properti baru ke objek componentsdengan nama komponen React saat ini ( this). Di properti ini, kami merujuk ke komponen this. Sekarang, setiap kali kita mengakses objek komponen, kita akan memiliki akses langsung ke komponen tersebut <ContentArea>.
Mari kita lihat apa yang terjadi di bagian bawah hierarki. Komponennya
<TicketDetail>bisa seperti ini:
// ticket.detail.js
import components from './components';
import React from 'react';
export default class TicketDetail extends React.Component {
render() {
components.ContentArea.consoleLog('it works');
return <div>Here are the ticket details.</div>;
}
}
Inilah yang terjadi. Setiap kali komponen dirender ,
TicketDetailfungsi consoleLog()yang disimpan dalam komponen akan dipanggil ContentArea.
Perhatikan bahwa fungsi tersebut
consoleLog()tidak melewati seluruh hierarki melalui props. Faktanya, fungsi tersebut consoleLog()tidak diteruskan ke mana pun - tidak ke mana pun - ke komponen apa pun.
Namun itu
TicketDetailbisa memanggil fungsi consoleLog()yang disimpan di ContentArea, karena kami melakukan dua hal:
ContentAreaSaat dimuat, komponen menambahkan tautan ke dirinya sendiri ke objek bersama komponen.TicketDetailSaat dimuat, komponen mengimpor objek bersamacomponents, yaitu memiliki akses langsung ke komponenContentArea, terlepas dari kenyataan bahwa propertiContentAreatidak diteruskan ke komponenTicketDetailmelalui props.
Pendekatan ini tidak hanya bekerja dengan fungsi / callback. Ini dapat digunakan untuk menanyakan langsung nilai variabel negara. Bayangkan apa yang
<ContentArea>terlihat seperti ini:
// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';
export default class ContentArea extends React.Component {
constructor(props) {
super(props);
this.state = { reduxSucks:true };
components.ContentArea = this;
}
render() {
return <MainContentArea/>;
}
}
Lalu kita bisa menulis
<TicketDetail>seperti ini:
// ticket.detail.js
import components from './components';
import React from 'react';
export default class TicketDetail extends React.Component {
render() {
if (components.ContentArea.state.reduxSucks === true) {
console.log('Yep, Redux is da sux');
}
return <div>Here are the ticket details.</div>;
}
}
Sekarang, setiap kali komponen di-render
<TicketDetail, itu akan mencari nilai variabel state.reduxSucksdi <ContentArea>. Jika variabel mengembalikan nilai true, fungsi console.log()akan mencetak pesan ke konsol. Ini akan terjadi bahkan jika nilai variabel ContentArea.state.reduxSuckstidak pernah diturunkan pohon - ke salah satu komponen - melalui props. Jadi, dengan satu objek JS sederhana yang mendasari yang berada di luar siklus hidup React standar, kita dapat membuatnya sehingga setiap turunan dapat membaca variabel status langsung dari induk mana pun yang dimuat ke dalam objek komponen. Kita bahkan bisa memanggil fungsi komponen induk pada turunannya.
Kemampuan untuk memanggil suatu fungsi secara langsung dalam komponen anak berarti bahwa kita dapat mengubah status komponen induk langsung dari anaknya. Contohnya seperti ini.
Pertama, di komponen,
<ContentArea>kita akan membuat fungsi sederhana yang mengubah nilai variabel reduxSucks.
// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';
export default class ContentArea extends React.Component {
constructor(props) {
super(props);
this.state = { reduxSucks:true };
components.ContentArea = this;
}
toggleReduxSucks() {
this.setState((previousState, props) => {
return { reduxSucks: !previousState.reduxSucks };
});
}
render() {
return <MainContentArea/>;
}
}
Kemudian, di dalam komponen,
<TicketDetail>kita akan memanggil metode ini melalui objek components:
// ticket.detail.js
import components from './components';
import React from 'react';
export default class TicketDetail extends React.Component {
render() {
if (components.ContentArea.state.reduxSucks === true) {
console.log('Yep, Redux is da sux');
}
return (
<>
<div>Here are the ticket details.</div>
<button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>
</>
);
}
}
Sekarang, setelah setiap render komponen,
<TicketDetail>pengguna akan dapat menekan tombol yang akan mengubah (toggle) nilai variabel ContentArea.state.reduxSuckssecara real time, bahkan jika fungsi ContentArea.toggleReduxSucks()tersebut tidak pernah diturunkan pohon melalui props.
Dengan pendekatan ini, komponen induk dapat memanggil fungsi tersebut langsung dari anaknya. Begini cara melakukannya. Komponen yang diperbarui
<ContentArea>akan terlihat seperti ini:
// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';
export default class ContentArea extends React.Component {
constructor(props) {
super(props);
this.state = { reduxSucks:true };
components.ContentArea = this;
}
toggleReduxSucks() {
this.setState((previousState, props) => {
return { reduxSucks: !previousState.reduxSucks };
});
components.TicketTable.incrementReduxSucksHasBeenToggledXTimes();
}
render() {
return <MainContentArea/>;
}
}
Sekarang mari tambahkan logika ke komponen
<TicketTable>. Seperti ini:
// ticket.table.js
import components from './components';
import React from 'react';
import TicketRow from './ticket.row';
export default class TicketTable extends React.Component {
constructor(props) {
super(props);
this.state = { reduxSucksHasBeenToggledXTimes: 0 };
components.TicketTable = this;
}
incrementReduxSucksHasBeenToggledXTimes() {
this.setState((previousState, props) => {
return { reduxSucksHasBeenToggledXTimes: previousState.reduxSucksHasBeenToggledXTimes + 1};
});
}
render() {
const {reduxSucksHasBeenToggledXTimes} = this.state;
return (
<>
<div>The `reduxSucks` value has been toggled {reduxSucksHasBeenToggledXTimes} times</div>
<TicketRow data={dataForTicket1}/>
<TicketRow data={dataForTicket2}/>
<TicketRow data={dataForTicket3}/>
</>
);
}
}
Hasilnya, komponen
<TicketDetail>tidak berubah. Ini masih terlihat seperti ini:
// ticket.detail.js
import components from './components';
import React from 'react';
export default class TicketDetail extends React.Component {
render() {
if (components.ContentArea.state.reduxSucks === true) {
console.log('Yep, Redux is da sux');
}
return (
<>
<div>Here are the ticket details.</div>
<button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>
</>
);
}
}
Pernahkah Anda memperhatikan keanehan yang terkait dengan ketiga kelas ini? Dalam hierarki aplikasi kita
ContentArea, ini adalah komponen induk TicketTable, yang merupakan komponen induk untuk TicketDetail. Artinya ketika kita me-mount sebuah komponen ContentArea, ia belum "tahu" keberadaannya TicketTable. Dan fungsi yang toggleReduxSucks()tertulis di dalamnya ContentAreasecara implisit memanggil fungsi anak :.
incrementReduxSucksHasBeenToggledXTimes()Ternyata kode tersebut tidak akan berfungsi , bukan?
Tapi tidak.
Lihat. Kami telah membuat beberapa level dalam aplikasi, dan hanya ada satu cara untuk memanggil fungsi tersebut
toggleReduxSucks(). Seperti ini.
- Kami memasang dan merender
ContentArea. - Selama proses ini, referensi ke komponen dimuat ke objek komponen
ContentArea. - Hasilnya dipasang dan dirender
TicketTable. - Selama proses ini, referensi ke komponen dimuat ke objek komponen
TicketTable. - Hasilnya dipasang dan dirender
TicketDetail. - « reduxSucks» (Toggle reduxSucks).
- « reduxSucks».
-
toggleReduxSucks(),ContentArea. -
incrementReduxSucksHasBeenToggledXTimes()TicketTable. - , , « reduxSucks»,
TicketTablecomponents.toggleReduxSucks()ContentAreaincrementReduxSucksHasBeenToggledXTimes(),TicketTable, components.
Ternyata hierarki aplikasi kita memungkinkan kita untuk menambahkan
ContentAreaalgoritme ke komponen yang akan memanggil fungsi dari komponen anak, padahal komponen ContentAreatersebut tidak mengetahui keberadaan komponen tersebut saat dipasangTicketTable .
Alat Manajemen Kekayaan - Buang
Seperti yang saya jelaskan, saya sangat yakin bahwa Redux bukan tandingan MobX. Dan ketika saya mendapat hak istimewa untuk mengerjakan sebuah proyek dari awal (sayangnya tidak sering), saya selalu berkampanye untuk MobX. Bukan untuk Redux. Tetapi ketika saya mengembangkan aplikasi saya sendiri , saya jarang menggunakan alat manajemen negara pihak ketiga sama sekali - hampir tidak pernah . Sebaliknya, saya hanya menyimpan objek / komponen jika memungkinkan. Dan jika pendekatan ini tidak berhasil, saya sering kembali ke solusi default di React, yaitu, saya hanya meneruskan variabel function / state melalui props.
"Masalah" yang diketahui dengan pendekatan ini
Saya sangat menyadari bahwa ide saya untuk menyimpan objek yang mendasari
componentstidak selalu cocok untuk menyelesaikan masalah status / fungsi bersama. Terkadang pendekatan ini bisa ... memainkan lelucon yang kejam . Atau mungkin tidak berhasil sama sekali . Berikut sesuatu yang perlu diingat.
- Ini bekerja paling baik dengan para lajang .
Misalnya, dalam hierarki kami, komponen <TicketTable> berisi komponen <TicketRow> dengan hubungan nol-ke-banyak. Jika Anda ingin menyimpan referensi ke setiap komponen potensial di dalam komponen <TicketRow> (dan komponen anak <TicketDetail> mereka) di cache komponen, Anda harus menyimpannya dalam larik, dan ini bisa menjadi rumit. Saya selalu menghindari ini. - components , / , components. .
, . , , . / , , components. - ,
components, (setState()),setState(), .
Sekarang setelah saya menjelaskan pendekatan saya dan beberapa keterbatasannya, saya harus memperingatkan Anda. Sejak saya menemukan pendekatan ini, saya telah membagikannya dengan orang-orang yang menganggap diri mereka sebagai pengembang React profesional. Setiap kali mereka menjawab hal yang sama:
Hmm ... Jangan lakukan itu. Mereka mengerutkan kening dan bertingkah seperti saya baru saja mengacaukan suasana. Sesuatu dalam pendekatan saya tampaknya bagi mereka ... salah . Pada saat yang sama, tidak ada yang belum menjelaskan kepada saya, berdasarkan pengalaman praktis yang kaya mereka, apa sebenarnya yang salah. Hanya saja semua orang menganggap pendekatan saya ... penghujatan .
Oleh karena itu, meskipun Anda menyukai pendekatan ini atau jika Anda merasa nyaman dalam beberapa situasi, saya tidak menyarankanmembicarakannya dalam sebuah wawancara jika Anda ingin mendapatkan pekerjaan sebagai pengembang React. Saya pikir bahkan hanya dengan berbicara dengan pengembang React lainnya, Anda perlu berpikir jutaan kali sebelum berbicara tentang metode ini, atau mungkin lebih baik tidak mengatakan apa-apa.
Saya telah menemukan bahwa pengembang JS - dan khususnya pengembang React - dapat menjadi terlalu kategoris . Kadang-kadang mereka menjelaskan mengapa Pendekatan A "salah" dan Pendekatan B "benar". Tetapi dalam banyak kasus, mereka hanya melihat sepotong kode dan menyatakannya "buruk", bahkan jika mereka sendiri tidak dapat menjelaskan mengapa.
Jadi mengapa pendekatan ini sangat mengganggu bagi pengembang React?
Seperti yang saya katakan, tidak ada rekan-rekan saya bisa cukup menjawab mengapa metode saya buruk. Dan jika ada yang mau menghormati saya dengan sebuah jawaban, biasanya itu adalah salah satu dari alasan berikut (ada beberapa di antaranya).
- , .
.... , , Redux ( MobX, ) / React-. , . — . , /, . : ,components. , /components, / , components. /,components, components . , , . , , Redux, MobX, - . - React « ». … .
… . ? , . — - « » « », , , . React, . , . . « », . , React 100 %, ( ) , .
, ?
Saya menulis posting ini karena saya telah menggunakan pendekatan ini selama bertahun-tahun (dalam proyek pribadi). Dan itu bekerja dengan baik . Tetapi setiap kali saya keluar dari gelembung pribadi saya dan mencoba melakukan percakapan cerdas tentang pendekatan ini dengan pengembang React pihak ketiga lainnya , saya hanya menemukan pernyataan kategoris dan penilaian konyol tentang "standar industri". Apakah
pendekatan ini benar-benar buruk ? Sungguh. Saya ingin tahu. Jika ini benar-benar sebuah "anti-pola", saya akan sangat berterima kasih kepada mereka yang akan membenarkan ketidaktepatannya. Jawaban "Saya tidak terbiasa dengan ini" tidak akan cocok untuk saya. Tidak, saya tidak terobsesi dengan metode ini. Saya tidak menyarankan bahwa ini adalah obat mujarab untuk pengembang React. Dan saya akui bahwa itu tidak berhasil untuk semua situasi. Tapi mungkinAdakah yang bisa menjelaskan kepada saya apa yang salah dengan itu?
Saya benar-benar ingin mengetahui pendapat Anda tentang masalah ini - bahkan jika Anda membuat saya hancur berkeping-keping.