Membuat Pustaka Validasi React Anda Sendiri: Dasar-dasar (Bagian 1)
Diterbitkan: 2022-03-10Saya selalu berpikir perpustakaan validasi formulir cukup keren. Saya tahu, ini adalah minat khusus untuk dimiliki — tetapi kami sangat sering menggunakannya! Setidaknya dalam pekerjaan saya — sebagian besar yang saya lakukan adalah membuat formulir yang kurang lebih kompleks dengan aturan validasi yang bergantung pada pilihan dan jalur sebelumnya. Memahami cara kerja pustaka validasi formulir adalah yang terpenting.
Tahun lalu, saya menulis satu perpustakaan validasi formulir tersebut. Saya menamakannya "Kalidasi", dan Anda dapat membaca posting blog pengantar di sini. Ini adalah perpustakaan bagus yang menawarkan banyak fleksibilitas dan menggunakan pendekatan yang sedikit berbeda dari yang lain di pasar. Namun, ada banyak perpustakaan hebat lainnya di luar sana — milik saya bekerja dengan baik untuk kebutuhan kami .
Hari ini, saya akan menunjukkan kepada Anda bagaimana menulis library validasi Anda sendiri untuk React. Kami akan melalui proses langkah demi langkah, dan Anda akan menemukan contoh CodeSandbox saat kami melanjutkan. Di akhir artikel ini, Anda akan mengetahui cara menulis pustaka validasi Anda sendiri, atau setidaknya memiliki pemahaman yang lebih dalam tentang bagaimana pustaka lain mengimplementasikan "keajaiban validasi".
- Bagian 1: Dasar-dasar
- Bagian 2: Fitur
- Bagian 3: Pengalaman
Langkah 1: Merancang API
Langkah pertama untuk membuat perpustakaan apa pun adalah merancang bagaimana perpustakaan itu akan digunakan. Ini meletakkan dasar untuk banyak pekerjaan yang akan datang, dan menurut saya, ini adalah satu-satunya keputusan terpenting yang akan Anda buat di perpustakaan Anda.
Sangat penting untuk membuat API yang "mudah digunakan", namun cukup fleksibel untuk memungkinkan peningkatan di masa mendatang dan kasus penggunaan lanjutan. Kami akan mencoba untuk mencapai kedua tujuan ini.
Kami akan membuat kait khusus yang akan menerima satu objek konfigurasi. Ini akan memungkinkan opsi masa depan untuk dilewati tanpa memperkenalkan perubahan yang melanggar.
Catatan Tentang Kait
Hooks adalah cara yang cukup baru untuk menulis React. Jika Anda pernah menulis React sebelumnya, Anda mungkin tidak mengenali beberapa konsep ini. Dalam hal ini, silakan lihat dokumentasi resmi. Ini ditulis dengan sangat baik, dan membawa Anda melalui dasar-dasar yang perlu Anda ketahui.
Kita akan memanggil custom hook useValidation
untuk saat ini. Penggunaannya mungkin terlihat seperti ini:
const config = { fields: { username: { isRequired: { message: 'Please fill out a username' }, }, password: { isRequired: { message: 'Please fill out a password' }, isMinLength: { value: 6, message: 'Please make it more secure' } } }, onSubmit: e => { /* handle submit */ } }; const { getFieldProps, getFormProps, errors } = useValidation(config);
Objek config
menerima prop fields
, yang menyiapkan aturan validasi untuk setiap bidang. Selain itu, ia menerima panggilan balik saat formulir dikirimkan.
Objek fields
berisi kunci untuk setiap bidang yang ingin kita validasi. Setiap bidang memiliki konfigurasinya sendiri, di mana setiap kunci adalah nama validator, dan setiap nilai adalah properti konfigurasi untuk validator tersebut. Cara lain untuk menulis hal yang sama adalah:
{ fields: { fieldName: { oneValidator: { validatorRule: 'validator value' }, anotherValidator: { errorMessage: 'something is not as it should' } } } }
Kait useValidation
kami akan mengembalikan objek dengan beberapa properti — getFieldProps
, getFormProps
dan errors
. Dua fungsi pertama adalah apa yang Kent C. Dodds sebut "pengambil prop" (lihat di sini untuk artikel bagus tentang itu), dan digunakan untuk mendapatkan properti yang relevan untuk bidang formulir atau tag formulir tertentu. Prop errors
adalah objek dengan pesan kesalahan apa pun, dikunci per bidang.
Penggunaan ini akan terlihat seperti ini:
const config = { ... }; // like above const LoginForm = props => { const { getFieldProps, getFormProps, errors } = useValidation(config); return ( <form {...getFormProps()}> <label> Username<br/> <input {...getFieldProps('username')} /> {errors.username && <div className="error">{errors.username}</div>} </label> <label> Password<br/> <input {...getFieldProps('password')} /> {errors.password && <div className="error">{errors.password}</div>} </label> <button type="submit">Submit my form</button> </form> ); };
Baiklah! Jadi kami telah memakukan API.
- Lihat demo CodeSandbox
Perhatikan bahwa kami juga telah membuat implementasi tiruan dari kait useValidation
. Untuk saat ini, ini hanya mengembalikan objek dengan objek dan fungsi yang kami perlukan untuk berada di sana, jadi kami tidak merusak implementasi sampel kami.
Menyimpan Status Formulir
Hal pertama yang perlu kita lakukan adalah menyimpan semua status formulir di kait khusus kita. Kita perlu mengingat nilai setiap bidang, pesan kesalahan apa pun dan apakah formulir telah dikirimkan atau belum. Kami akan menggunakan kait useReducer
untuk ini karena memungkinkan fleksibilitas paling tinggi (dan lebih sedikit boilerplate). Jika Anda pernah menggunakan Redux, Anda akan melihat beberapa konsep yang sudah dikenal — dan jika tidak, kami akan menjelaskannya seiring berjalannya waktu! Kami akan memulai dengan menulis peredam, yang diteruskan ke kait useReducer
:
const initialState = { values: {}, errors: {}, submitted: false, }; function validationReducer(state, action) { switch(action.type) { case 'change': const values = { ...state.values, ...action.payload }; return { ...state, values, }; case 'submit': return { ...state, submitted: true }; default: throw new Error('Unknown action type'); } }
Apa itu Peredam?
Peredam adalah fungsi yang menerima objek nilai dan "aksi" dan mengembalikan versi yang diperbesar dari objek nilai.
Tindakan adalah objek JavaScript biasa dengan properti type
. Kami menggunakan pernyataan switch
untuk menangani setiap jenis tindakan yang mungkin.
"Objek nilai" sering disebut sebagai status , dan dalam kasus kami, ini adalah status logika validasi kami.
Status kami terdiri dari tiga bagian data — values
(nilai saat ini dari bidang formulir kami), errors
(kumpulan pesan kesalahan saat ini) dan sebuah bendera isSubmitted
menunjukkan apakah formulir kami telah dikirimkan setidaknya satu kali.
Untuk menyimpan status formulir kita, kita perlu mengimplementasikan beberapa bagian dari kait useValidation
kita. Saat kita memanggil metode getFieldProps
kita, kita perlu mengembalikan sebuah objek dengan nilai dari field tersebut, sebuah change-handler ketika berubah, dan sebuah prop nama untuk melacak field yang mana.
function validationReducer(state, action) { // Like above } const initialState = { /* like above */ }; const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); return { errors: state.errors, getFormProps: e => {}, getFieldProps: fieldName => ({ onChange: e => { if (!config.fields[fieldName]) { return; } dispatch({ type: 'change', payload: { [fieldName]: e.target.value } }); }, name: fieldName, value: state.values[fieldName], }), }; };
Metode getFieldProps
sekarang mengembalikan alat peraga yang diperlukan untuk setiap bidang. Saat peristiwa perubahan dipicu, kami memastikan bidang itu ada dalam konfigurasi validasi kami, dan kemudian memberi tahu peredam kami bahwa tindakan change
telah terjadi. Peredam akan menangani perubahan status validasi.
- Lihat demo CodeSandbox
Memvalidasi Formulir Kami
Pustaka validasi formulir kami terlihat bagus, tetapi tidak melakukan banyak hal dalam hal memvalidasi nilai formulir kami! Mari kita perbaiki itu.
Kami akan memvalidasi semua bidang pada setiap acara perubahan. Ini mungkin kedengarannya tidak terlalu efisien, tetapi dalam aplikasi dunia nyata yang saya temui, itu tidak terlalu menjadi masalah.
Catatan, kami tidak mengatakan Anda harus menunjukkan setiap kesalahan pada setiap perubahan. Kami akan meninjau kembali cara menampilkan kesalahan hanya saat Anda mengirim atau menavigasi keluar dari bidang, nanti di artikel ini.
Cara Memilih Fungsi Validator
Ketika berbicara tentang validator, ada banyak perpustakaan di luar sana yang mengimplementasikan semua metode validasi yang Anda perlukan. Anda juga dapat menulis sendiri jika Anda mau. Ini adalah latihan yang menyenangkan!
Untuk proyek ini, kita akan menggunakan satu set validator yang saya tulis beberapa waktu lalu — calidators
. Validator ini memiliki API berikut:
function isRequired(config) { return function(value) { if (value === '') { return config.message; } else { return null; } }; } // or the same, but terser const isRequired = config => value => value === '' ? config.message : null;
Dengan kata lain, setiap validator menerima objek konfigurasi dan mengembalikan validator yang dikonfigurasi sepenuhnya. Ketika fungsi itu dipanggil dengan sebuah nilai, ia mengembalikan message
prop jika nilainya tidak valid, atau null
jika valid. Anda dapat melihat bagaimana beberapa validator ini diimplementasikan dengan melihat kode sumbernya.
Untuk mengakses validator ini, instal paket calidators
dengan npm install calidators
.
Validasi satu bidang
Ingat konfigurasi yang kami berikan ke objek useValidation
kami? Ini terlihat seperti ini:
{ fields: { username: { isRequired: { message: 'Please fill out a username' }, }, password: { isRequired: { message: 'Please fill out a password' }, isMinLength: { value: 6, message: 'Please make it more secure' } } }, // more stuff }
Untuk menyederhanakan implementasi kita, mari kita asumsikan kita hanya memiliki satu bidang untuk divalidasi. Kami akan memeriksa setiap kunci dari objek konfigurasi bidang, dan menjalankan validator satu per satu sampai kami menemukan kesalahan atau selesai memvalidasi.
import * as validators from 'calidators'; function validateField(fieldValue = '', fieldConfig) { for (let validatorName in fieldConfig) { const validatorConfig = fieldConfig[validatorName]; const validator = validators[validatorName]; const configuredValidator = validator(validatorConfig); const errorMessage = configuredValidator(fieldValue); if (errorMessage) { return errorMessage; } } return null; }
Di sini, kami telah menulis fungsi validateField
, yang menerima nilai untuk divalidasi dan konfigurasi validator untuk bidang itu. Kami mengulang semua validator, memberikan mereka konfigurasi untuk validator itu, dan menjalankannya. Jika kami mendapatkan pesan kesalahan, kami melewatkan validator lainnya dan kembali. Jika tidak, kami mencoba validator berikutnya.
Catatan: Pada API validator
Jika Anda memilih validator yang berbeda dengan API yang berbeda (seperti validator.js
yang sangat populer), bagian kode Anda ini mungkin terlihat sedikit berbeda. Namun, untuk singkatnya, kami membiarkan bagian itu menjadi latihan yang diserahkan kepada pembaca.
Catatan: Aktif untuk…dalam loop
Belum pernah menggunakan for...in
loops sebelumnya? Tidak apa-apa, ini juga pertama kalinya bagiku! Pada dasarnya, ini mengulangi kunci dalam suatu objek. Anda dapat membaca lebih lanjut tentang mereka di MDN.
Validasi semua bidang
Sekarang setelah kami memvalidasi satu bidang, kami seharusnya dapat memvalidasi semua bidang tanpa terlalu banyak kesulitan.
function validateField(fieldValue = '', fieldConfig) { // as before } function validateFields(fieldValues, fieldConfigs) { const errors = {}; for (let fieldName in fieldConfigs) { const fieldConfig = fieldConfigs[fieldName]; const fieldValue = fieldValues[fieldName]; errors[fieldName] = validateField(fieldValue, fieldConfig); } return errors; }
Kami telah menulis fungsi validateFields
yang menerima semua nilai bidang dan konfigurasi seluruh bidang. Kami mengulang setiap nama bidang dalam konfigurasi dan memvalidasi bidang itu dengan objek dan nilai konfigurasinya.
Berikutnya: Beri tahu peredam kami
Baiklah, jadi sekarang kita memiliki fungsi ini yang memvalidasi semua barang kita. Mari kita tarik ke dalam sisa kode kita!
Pertama, kita akan menambahkan penangan tindakan validate
ke validationReducer
kita.
function validationReducer(state, action) { switch (action.type) { case 'change': // as before case 'submit': // as before case 'validate': return { ...state, errors: action.payload }; default: throw new Error('Unknown action type'); } }
Setiap kali kami memicu tindakan validate
, kami mengganti kesalahan di negara kami dengan apa pun yang diteruskan di samping tindakan.
Selanjutnya, kita akan memicu logika validasi kita dari hook useEffect
:
const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); useEffect(() => { const errors = validateFields(state.fields, config.fields); dispatch({ type: 'validate', payload: errors }); }, [state.fields, config.fields]); return { // as before }; };
Kait useEffect
ini berjalan setiap kali state.fields
atau config.fields
kami berubah, selain pada pemasangan pertama.
Waspadalah Terhadap Bug
Ada bug super halus dalam kode di atas. Kami telah menetapkan bahwa kait useEffect
kami hanya boleh dijalankan kembali setiap kali state.fields
atau config.fields
berubah. Ternyata, "perubahan" tidak selalu berarti perubahan nilai! useEffect
menggunakan Object.is
untuk memastikan kesetaraan antara objek, yang pada gilirannya menggunakan kesetaraan referensi. Yaitu — jika Anda melewatkan objek baru dengan konten yang sama, itu tidak akan sama (karena objek itu sendiri baru).
state.fields
dikembalikan dari useReducer
, yang menjamin kesetaraan referensi ini, tetapi config
kami ditentukan sebaris dalam komponen fungsi kami. Itu berarti objek dibuat ulang pada setiap render, yang pada gilirannya akan memicu useEffect
di atas!
Untuk mengatasi ini, kita perlu menggunakan perpustakaan use-deep-compare-effect
oleh Kent C. Dodds. Anda menginstalnya dengan npm install use-deep-compare-effect
, dan mengganti panggilan useEffect
Anda dengan ini sebagai gantinya. Ini memastikan kami melakukan pemeriksaan kesetaraan mendalam alih-alih pemeriksaan kesetaraan referensi.
Kode Anda sekarang akan terlihat seperti ini:
import useDeepCompareEffect from 'use-deep-compare-effect'; const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); useDeepCompareEffect(() => { const errors = validateFields(state.fields, config.fields); dispatch({ type: 'validate', payload: errors }); }, [state.fields, config.fields]); return { // as before }; };
Catatan Tentang penggunaanEfek
Ternyata, useEffect
adalah fungsi yang cukup menarik. Dan Abramov menulis artikel panjang yang sangat bagus tentang seluk-beluk useEffect
jika Anda tertarik untuk mempelajari semua yang ada tentang hook ini.
Sekarang semuanya mulai terlihat seperti perpustakaan validasi!
- Lihat demo CodeSandbox
Penanganan Pengajuan Formulir
Bagian terakhir dari pustaka validasi formulir dasar kami menangani apa yang terjadi saat kami mengirimkan formulir. Saat ini, itu memuat ulang halaman, dan tidak ada yang terjadi. Itu tidak optimal. Kami ingin mencegah perilaku browser default dalam hal formulir, dan menanganinya sendiri sebagai gantinya. Kami menempatkan logika ini di dalam fungsi getFormProps
prop getter:
const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); // as before return { getFormProps: () => ({ onSubmit: e => { e.preventDefault(); dispatch({ type: 'submit' }); if (config.onSubmit) { config.onSubmit(state); } }, }), // as before }; };
Kami mengubah fungsi getFormProps
kami untuk mengembalikan fungsi onSubmit
, yang dipicu setiap kali acara DOM submit
dipicu. Kami mencegah perilaku browser default, mengirimkan tindakan untuk memberi tahu peredam yang kami kirimkan, dan memanggil callback onSubmit
yang disediakan dengan seluruh status — jika disediakan.
Ringkasan
Kami di sana! Kami telah membuat pustaka validasi yang sederhana, dapat digunakan, dan cukup keren. Masih ada banyak pekerjaan yang harus dilakukan sebelum kita bisa mendominasi jalinan.
- Bagian 1: Dasar-dasar
- Bagian 2: Fitur
- Bagian 3: Pengalaman