DEV Community

Armedi
Armedi

Posted on • Originally published at Medium on

Menggunakan Vue Reactivity pada Komponen React

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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} />;
};
Enter fullscreen mode Exit fullscreen mode

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} />;
};
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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} />;
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Dari sini anda bisa lanjutkan bereksperimen untuk membuat global state management menggunakan modul reactivity dari Vue

source code:

codesandbox:

Top comments (0)