25 February 2024

Membuat react versi sendiri (PART 1)

Image by Jukan Tateisi

Intro bentar

Saya sudah membuat aplikasi selama beberapa tahun dan pada awalnya, kebanyakan aplikasi yang saya buat cuma pake HTML, CSS, dan Javascript. Belum pake framework yang aneh-aneh karena masih belajar juga. Setelah merasa cukup belajar JS akhirnya memutuskan untuk belajar salah satu framework javascript, dan framework yang paling poluler tentu saja ReactJs. Saya sudah pake react sejak 3 tahun lalu, dan saat ini pun kebanyakan kerjaan saya menggunakan React. Tapi sebenarnya saya sudah lama penasaran gimana caranya React mengolah code yang kita bikin menjadi javascript yang bisa di tampilkan di web. Jadi saya coba mengulik-ngulik bagaimana cara kerja react dibelakang layar sehingga bisa dijalankan di web.

Apa itu React

Jujur saya sendiri masih bingung kalo disuruh mendefinisikan React itu apa hehehe :). Dan juga banyak perdebatan apakah react itu library atau framework. Tapi saya sendiri condong setuju dengan yang bilang kalo react itu library rasa framework ya, walaupun kita punya kebebasan dalam menulis atau menentukan struktur projek kita tapi dalam beberapa hal yaa kita harus mengikuti aturan si reactnya. React sendiri hanya menghandle bagian antarmuka untuk device kita aja untuk kalo mau lebih jauh misal perlu routing ya harus install library lain yaitu react router atau ya gunakan saja framework yang menggunakan react macam nextjs atau remix.

Menjalankan aplikasi React

Untuk membuat applikasi react, biasanya kita akan membuatnya dengan CRA (creat-react-app) atau yang modern dengan menggunakan template bawaan vite atau jika ingin menggunakan framework biasanya kita menggunakan react dalam framework nextjs dan kawan-kawan. Tapi supaya lebih simpel dalam mempelajari react kali ini, saya hanya ingin menggunakan react dengan versi cdn nya, jadi kita hanya perlu import script react di HTML kita dan menulis kode react disitu tanpa bundler atau library yang lain.

<!doctype html>
<html lang="en">
  <head>
    <title></title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <body>
      <div id="root"></div>
      <script
        src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"
        integrity="sha512-8Q6Y9XnTbOE+JNvjBQwJ2H8S+UV4uA6hiRykhdtIyDYZ2TprdNmWOUaKdGzOhyr4dCyk287OejbPvwl7lrfqrQ=="
        crossorigin="anonymous"
        referrerpolicy="no-referrer"
      ></script>
      <script
        src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"
        integrity="sha512-MOCpqoRoisCTwJ8vQQiciZv0qcpROCidek3GTFS6KTk2+y7munJIlKCVkFCYY+p3ErYFXCjmFjnfTTRSC1OHWQ=="
        crossorigin="anonymous"
        referrerpolicy="no-referrer"
      ></script>

      <script>
        let root = document.getElementById("root");

        function App() {
          const [count, setCount] = React.useState(0);

          let button = React.createElement(
            "button",
            {
              onClick: () => setCount(count + 1),
            },
            "Click",
          );

          let text = React.createElement("p", null, count);

          let wrapper = React.createElement("div", null, button, text);

          return wrapper;
        }

        const rootDOM = ReactDOM.createRoot(root);

        rootDOM.render(React.createElement(App));
      </script>
    </body>
  </body>
</html>

Disini saya hanya menggunakan 2 library saja yaitu react dan reactDom. Fungsi reactDom disini untuk merender struktur HTML yang sudah kita buat dengan react ke browser. Untuk yang asing dengan kode diatas terutama bagian “React.createElement”, itulah react sebenarnya tanpa JSX. React yang sering kita gunakan biasanya dibuat bersama dengan JSX.

Pada kode diatas, createElement di React tujuannya buat bikin elemen atau bagian-bagian keren dalam aplikasi kita. Jadi, misalnya kita mau bikin container, text, tombol, atau apapun itu dalam tampilan, kita bisa pakai ini. Jadi, createElement ini mintanya tiga argumen: apa jenis elemennya (kayak h1, p, atau html tag lainnya), apa aja yang mau kita kasih ke elemen itu (macem-macem atribut atau properti kaya class atau event), terus juga apa isi di dalamnya (kaya tulisan apa yang mau dimunculin, atau elemen lain yang mau dimasukin ke dalamnya). Struktur dom yang kita buat dengan createElemetn dinamakan “Virtual DOM”. Seperti namanya, virtual dom ini fungsinya ya sebagai representasi virtual dari dom atau elemen sebenarnya yang ada di web.

Setelah virtual dom nya selesai dibuat, kita cuma perlu nampilin element yang udah dibuat ke layar web kita. di React, tool yang sering dipake buat menampilkan virtual dom ke yang sudah kita buat ke halaman web adalah dengan ReactDOM. Nah, disini kita bisa bedakan kalo reactDOM itu bukan bagian dari react karena saat ini react bukan hanya digunakan untuk web tapi juga untuk mobile yaitu react native. React hanya menghandle logic untuk interfacenya saja sedangkan tool yang digunakan untuk menampilkan element-element tersebut ke layar device kita adalah hal yang terpisah.

Tapi di artikel ini saya mau buat juga fungsi yang dapat menampilkan virtual dom kita ke element biar kita bisa lihat hasil kerja keras kita di layar device.

Memahami hubungan JSX dan React

Hal pertama yang harus kita perlu ketahui adalah bagaimana React dan JSX bekerja, React yang kita gunakan sehari-hari menggunakan alat yang dinamakan JSX. JSX ini fungsinya untuk menulis code JS tapi dalam bentuk yang mirip seperti HTML. Nah, nantinya kode JSX itu akan diubah kedalam kode Javascript menggunakan alat khusus yang dinamakan transpiler. Salah satu tool transpiler yang paling terkenal di dunia JS adalah Babel. Jadi disini fungsi Babel adalah mengubah kode JSX kedalam bentuk code JS biasa yang nantinya akan digunakan oleh React untuk membangun antarmuka pengguna. Nah, sampai sini berarti dapat kita simpulkan kalo React itu tidak bekerja sendiri, tapi ada tool-tool lain yang digunakan untuk membuat code React seperti yang kita buat sehari-hari.

Jadi jelas ya kalo React pun bisa bekerja tanpa JSX, alias React dan JSX itu beda ya dan suatu entitas yang terpisah.

Mulai membuat “React”

Oke, setelah paham bahwa kita bisa buat aplikasi react tanpa JSX berarti kita sudah berhasil mengupas satu level abstraksi bagaimana react bekerja. Selanjutnya kita bakal coba mencoba mengimplementasi fungsi “createElement” dan juga “render” untuk bisa memahami lebih dalam gimana cara react mengubah kode yang tadi kita buat bisa tampil di layar browser kita.

Membuat fungsi “createElement”

Pada dasarnya, fungsi createElement itu cukup sederhana. Kita cuma perlu menerima parameter yang diberikan dan mengubahnya kedalam bentuk object yang bisa menggambarkan fungsi suatu element.

const react = {
  createTextElement: (str) => {
    return {
      type: "TEXT",
      value: str,
    };
  },

  createElement: function (tag, attrs, ...children) {
    return {
      tag: tag,
      attrs: attrs,
      children: children.map(child => typeof child == 'object' child : this.createTextElement(child))
    }
  }
}

Pada dasarnya ada dua jenis element yang perlu kita bangun, yang pertama adalah jika elemen yang dibuat adalah tag HTML pada umumnya, dan juga element teks. Jika kita jalankan fungsi diatas

const element = React.createElement("h1", null, "Hello, world");
console.log(element);
/*
{
  tag: "h1",
  attributes: null,
  children: [
    {
      tag: "TEXT",
      attributes: {
        nodeValue: "Hello, world"
      },
      children: []
    }
  ]
}
*/
const heading = React.createElement("h1", { className: "bold" }, "Hello, world");
const heading = React.createElement("p", null, "My name is Suyono");
const container = React.createElement("div", null, )
console.log(container);
/*
{
  tag: "div",
  attributes: null,
  children: [
    {
      tag: "h1",
      props: {
        className: "bold"
      },
      children: [
        {
          {
            tag: "TEXT",
            attributes: {
              nodeValue: "Hello, world"
            },
            children: []
          }
        }
      ]
    }
  ]
}
*/

Jadi kita bisa membentuk sebuah object yang menggambarkan sebuah HTML element yang akan kita tampilkan di layar nantinya.

Tapi, kita kan biasanya membuat aplikasi React menggunakan JSX. Untuk menggunakan JSX, kita memerlukan Babel, secara default, JSX pada Babel memerlukan React untuk mengubah JSX kedalam betuk object JS. untuk menggunakan JSX dalam React yang akan kita bangun kita cuma perlu bilang ke Babel kalo kita mau fungsi JSX di proses menggunakan fungsi yang kita buat bukan menggunakan React.

Sebelum menggunakan JSX, kita harus menambahkan library babel kedalam kode kita.

<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

Namun, kita juga harus memberi tahu babel kode mana yang akan di eksekusi oleh babel. Babel akan mendeteksi script dengan tipe text/babel atau text/jsx.

<script type="text/babel">
function App() {
  return (
    <h1>Hello world</h1>
  )
}
</script>

Secara default, babel akan meng-compile kode jsx menggunakan fungsi createElement milik react. Jadi jika kita memiliki variable bernama React yang memiliki fungsi createElement didalamanya, maka JSX akan di jalankan oleh fungsi createElement milik kita. Namun jika kita ingin mengganti perilaku default ini, kita hanya perlu menambahkan komentar didalam script kita yang menandakan jika jsx kita perlu dihandle oleh fungsi tertentu. Berikut adalah contoh jika kita ingin jsx dihandle oleh MyReact.createElement.

/** @jsx MyReact.createElement */
function App() {
  return (
    <div>
      <h1>Hello, world</h1>
      <p>My name is Suyono</p>
    </div>
  )
}

console.log(App());
// Akan mengeluarkan object yang sama seperti sebelumnya

Membuat fungsi “render”

Setelah berhasil membuat fungsi yang dapat membangun object yang menggamabarkan susunan element. Selanjutnya kita perlu membuat fungsi yang dapat menampilkan element yang kita buat ke layar browser. Fungsi “render” ini sebenernya cukup sederhana, kita cukup ambil tipe element yang udah kita bangun sebelumnya lalu buat dom element pake fungsi “createElement” dari javascript, js element yang dibuat baru akan kita append atau tambahkan ke root element di html, itulah kenapa di React selalu ada element dengan id root di “index.html” nya. Itu adalah tempat kita bakal naro element-element yang bakal di render.

render: function(vdom, parentDom) {
  // Check apakah tag vdom merupakan element teks untuk mementukan jenis element yang akan dibuat
  const dom = vdom.tag === 'TEXT' ? document.createTextNode("") : document.createElement(vdom.tag);

  // lakukan loop secara rekursif pada setiap element yang ada
  vdom.children.forEach(child => {
    const childDom = render(child, parentDom);
    dom.appendChild(childDom);
  });

  // Append dom yang telah dibuat ke parent dom
  parentDom.appendChild(dom);
  return dom;
}

const root = document.getElementById("#root");
function App() {
  return (
    <div>
      <h1>Hello, world</h1>
      <p>My name is Suyono</p>
    </div>
  )
}

React.render(App(), root);

Kalo kita mau nambahin fungsi render dalam react yang kita bangun, maka element dalam fungsi App akan tampil di layar browser.

Menambahkan “attributes” dan “events”

Attributes pada JSX biasanya berupa className, id, ataupun html attributes yang lain, sedangkan events berupa onClick, onChange, dan sebagainya. Pada JSX, attributtes seperti className akan ditransformasi kedalam “class” secara otomatis oleh Babel, sedangkan untuk menghandle event seperti onClick, kita harus melakukan transformasi secara manual.

Untuk menambahkan attributes, kita dapat menggunakan fungsi setAttribute milik JS. Sedangkan untuk events, kita dapat memasang event secara langsung dengan fungsi addEventListener.

setDomProperties: function(props, dom) => {
  const isEvent = (keys) => keys.substring(0, 2) === "on";
  Object.keys(props).forEach((key) => {
    if (isEvent(key)) {
      dom.addEventListener(
        key.substring(2, key.length).toLowerCase(),
        props[key]
      );
    } else {
      dom.setAttribute(key, props[key]);
    }
  });
},

Membuat function component

Dalam react yang biasanya kita gunakan, kita dapat menambahkan function sebagai component agar kode yang kita tulis lebih reusable. Untuk menambahkan function component pada react buatan kita, kita hanya perlu menambahkan if statement pada render function kita.

if (typeof vdom.type === "function") {
  return this.render(vdom.type(vdom.props), parentDom);
}

Setelah menambahkan function component dan fungsi untuk attribute dan event, fungsi render akan terlihat seperti ini

render: function (vdom, parentDom) {
  if (typeof vdom.type === "function")
    return this.render(vdom.type(vdom.props), parentDom);

  if (vdom.type === "TEXT") return document.createTextNode(vdom.value);
  const dom = document.createElement(vdom.type);

  if (vdom.props) {
    this.setDomProperties(vdom.props, dom);
  }

  vdom.children.forEach((element) => {
    const childDom = this.render(element, dom);
    dom.appendChild(childDom);
  });
  parentDom.appendChild(dom);
  return dom;
}

Kesimpulan

Untuk Part satu ini kita berhasil membuat framework sederhana yang dapat mengubah kode JSX kedalah Virtual DOM lalu menampilkannya di layar browser. Walau masih sangat sederhana dan masih sangat jauh dari implementasi react yang sebenarnya, namun implementasi ini sebenarnya adalah gambaran sederhana dari react yang sering kita gunakan sehari-hari.

Berikut adalah semua kode yang berhasil kita implementasikan pada artikel ini.

const React = {
  createElement: function (type, props, ...children) {
    return {
      type: type,
      props: props,
      children: children.map((child) =>
        typeof child === "object" ? child : this.createTextElement(child)
      ),
    };
  },

  createTextElement: (str) => {
    return {
      type: "TEXT",
      value: str,
    };
  },

  setDomProperties: (props, dom) => {
    const isEvent = (keys) => keys.substring(0, 2) === "on";
    Object.keys(props).forEach((key) => {
      if (isEvent(key)) {
        dom.addEventListener(
          key.substring(2, key.length).toLowerCase(),
          props[key]
        );
      } else {
        dom.setAttribute(key, props[key]);
      }
    });
  },

  render: function (vdom, parentDom) {
    if (typeof vdom.type === "function")
      return this.render(vdom.type(vdom.props), parentDom);

    if (vdom.type === "TEXT") return document.createTextNode(vdom.value);
    const dom = document.createElement(vdom.type);

    if (vdom.props) {
      this.setDomProperties(vdom.props, dom);
    }

    vdom.children.forEach((element) => {
      const childDom = this.render(element, dom);
      dom.appendChild(childDom);
    });
    parentDom.appendChild(dom);
    return dom;
  },

};

function Children({ name }) {
  return <h1>My name is: {name}</h1>;
}

function App() {
  return (
    <div className="asd" id="id">
      <p>Hello, world</p>
      <Children name={name} />
    </div>
  );
}

React.render(<App />, document.getElementById("root"));