Bagi anda yang berkecimpung di dunia frontend web development, pasti sudah tidak asing dengan nama React atau Vue. Dua library yang paling populer saat ini untuk membangun aplikasi web modern.
Para penggemar framework Vue pun pastinya sudah tahu bahwa tidak lama lagi Vue akan merilis versi 3. Versi baru ini tidak hanya menambahkan fitur baru, tapi benar-benar rewrite dari awal (Lebih lanjut: https://increment.com/frontend/making-vue-3/).
Salah satu fitur baru di Vue 3 yang mungkin paling banyak disorot adalah Composition API (Tutorial cara menggunakan Composition API pernah dibuat oleh teman saya Semmi Verian di video youtube-nya). Namun hal yang lebih menarik bagi saya pribadi adalah model reaktivitas baru yang sama sekali berbeda dengan versi sebelumnya.
Lebih menariknya lagi, modul reaktivitas ini dibuat pada package terpisah, yang artinya tentu saja bisa kita pakai pada aplikasi lain tanpa harus menggunakan framework Vue.
Nah karena saya sama-sama menyukai React dan Vue, jadi saya memutuskan untuk mencoba modul @vue/reactivity pada aplikasi yang dibuat menggunakan React.
Tujuan akhir dari tulisan ini adalah membahas bagaimana kita bisa memutasi data tanpa kehilangan reaktivitas dari React dengan menggunakan package @vue/reactivity. Cara seperti ini bukanlah hal baru, tapi juga digunakan oleh library seperti mobx-react (dengan observable dari mobx).
Mutable VS Immutable
Mutability adalah perbedaan mendasar antara React dengan Vue. React memilih jalur full immutable data, sedangkan Vue memilih jalur mutable data.
Lalu apa perbedaannya?
Sebelumnya kita bahas dulu sedikit tentang declarative programming. Dengan library atau framework modern, web developer tidak lagi perlu memikirkan bagaimana meng-update DOM. Cukup dengan mendeklarasikan apa yang dimau, selebihnya mengenai proses update DOM dan bagaimana terjadinya akan diatur oleh library yang dipakai (Artikel ini menjelaskannya dengan lebih baik).
React atau Vue akan mendeteksi perubahan data, dan kemudian melakukan perubahan yang sesuai pada UI. Dan ini terjadi terus-menerus secara otomatis. Atau dalam kata lain, komponen UI bersifat reaktif terhadap perubahan data.
Nah, di sini lah perbedaan antara immutability-nya React dengan mutability Vue. React dengan cara yang sangat sederhana mendeteksi perubahan data menggunakan Object.is. Itulah mengapa kode di bawah tidak akan berjalan sesuai yang diharapkan.
// in react component
const [name, setName] = useState({first: 'John', last: 'Doe'})
const handleChange = (event) => {
name.first = event.target.value
}
Ketika anda melakukan mutasi terhadap data nameseperti kode diatas, React tidak akan melihat perubahan, sehingga rerender pun tidak akan pernah terjadi. Karena object name masih merupakan object yang sama dengan sebelumnya.
Vue menggunakan cara berbeda yang lebih kompleks untuk mendeteksi perubahan data (lebih jelasnya bisa disimak langsung di dokumentasi resminya). Intinya adalah data pada Vue bisa dimutasi, dan dengan cerdas Vue akan mendeteksi perubahan kemudian melakukan update yang sesuai.
Let’s start playing
Aplikasi yang akan kita buat adalah aplikasi counter dengan fitur increment dan decrement, serta ada satu kolom input teks.
Scroll ke paling bawah jika anda mau langsung melihat source code dan hasil jadinya
npm install @vue/reactivity
Pertama kita akan buat sebuah fungsi HOC bernama setup yang lebih kurang berfungsi sama dengan setup pada composition API Vue. Penggunaanya akan seperti ini:
import React, { useMemo } from 'react';
import { reactive, ref } from '@vue/reactivity';
const setup = (setupFn) => (Component) => (props) => {
const setupProps = useMemo(setupFn, []);
// our little trick here
return <Component {...props} {...setupProps} />;
};
const App = (props) => {
// This is the react component
};
export default setup(() => {
const count = ref(0);
const name = reactive({ first: '' });
return { count, name };
})(App);
Fungsi setup akan memasukkan return value dari setupFn sebagai props pada komponen. Di sini kita gunakan useMemo agar invokasi hanya terjadi sekali ketika mounting saja.
Nah, sekarang bagaimana caranya untuk “memaksa” komponen agar rerender ketika props ini dimutasi?
@vue/reactivity menyediakan fungsi effect yang fungsinya adalah melakukan “sesuatu” (dengan meng-invoke callback yang diberikan) setiap kali value reaktif yang dipakai di dalam fungsi itu dimutasi. Fungsi effect ini akan kita tempatkan di dalam hooks useEffect React. Jangan lupa untuk menghentikan effect ketika komponen di-unmout dengan me-return sebuah fungsi untuk stop.
import React, { useMemo } from 'react';
import { reactive, ref, effect, stop } from '@vue/reactivity';
const setup = (setupFn) => (Component) => (props) => {
const setupProps = useMemo(setupFn, []);
useEffect(() => {
const reactiveEffect = effect(() => {
// here we can do something to trigger rerender
});
return () => stop(reactiveEffect);
}, [setupProps]);
return <Component {...props} {...setupProps} />;
};
Untuk “memaksa” rerender kita akan buat satu state yang value-nya akan di-update ketika terjadi mutasi data.
const setup = (setupFn) => (Component) => (props) => {
const setupProps = useMemo(setupFn, []);
const [, setTick] = useState(0);
useEffect(() => {
const reactiveEffect = effect(() => {
// Only one more thing to do here
setTick((tick) => tick + 1);
});
return () => stop(reactiveEffect);
}, [setupProps]);
return <Component {...props} {...setupProps} />;
};
Oke, tinggal satu langkah lagi
Sebelumnya saya katakan bahwa fungsi effect akan meng-invoke callback yang diberikan setiap kali value reaktif yang dipakai di dalam fungsi itu dimutasi. Terus bagaimana caranya fungsi ini tau ada mutasi?
Sangat sederhana sekali. Setiap value yang diakses di dalamnya akan secara otomatis di-track. Ya, cukup diakses saja!
useEffect(() => {
const reactiveEffect = effect(() => {
setupProps.count.value;
setupProps.name.first;
setTick((tick) => tick + 1);
});
return () => stop(reactiveEffect);
}, [setupProps]);
Karena di dalam komponen kita akan menggunakan value count dan name.first, dua value ini perlu diakses terlebih dahulu di dalam fungsi effect agar komponen bisa melacak mutasi dan melakukan rerender setiap kali ada perubahan.
Oke, cara diatas sebenarnya sudah “benar”. Tapi akan mengakibatkan error no-unused-expressions pada linter, dan juga membuat fungsi setup hanya bisa dipakai untuk satu komponen ini saja.
Sekarang mari kita generalisasi menjadi fungsi access yang secara rekursif akan mengakses setiap properti di dalam object
import React, { useState, useEffect, useMemo } from 'react';
import { reactive, ref, isRef, effect, stop } from '@vue/reactivity';
const access = (object) => {
if (isRef(object)) {
access(object.value);
} else if (typeof object === 'object') {
for (let key of Object.keys(object)) {
access(object[key]);
}
}
};
const setup = (setupFn) => (Component) => (props) => {
const setupProps = useMemo(setupFn, []);
const [, setTick] = useState(0);
useEffect(() => {
const reactiveEffect = effect(() => {
access(setupProps);
setTick((tick) => tick + 1);
});
return () => stop(reactiveEffect);
}, [setupProps]);
return <Component {...props} {...setupProps} />;
};
Akhirnya sekarang di dalam komponen React kita bisa melakukan update data dengan cara memutasinya seperti dibawah ini. Perhatikan onChange pada input dan onClick pada button.
function App({ count, name }) {
return (
<div>
<input
placeholder="Input your name"
onChange={(e) => (name.first = e.target.value)}
/>
<div>
<div>Hi, {name.first}</div>
<div>{count.value}</div>
<div>
<span>
<button type="button" onClick={() => count.value--}>
Decrement
</button>
</span>
<span>
<button type="button" onClick={() => count.value++}>
Increment
</button>
</span>
</div>
</div>
</div>
);
}
Dari sini anda bisa lanjutkan bereksperimen untuk membuat global state management menggunakan modul reactivity dari Vue
source code:
codesandbox:
Top comments (0)