Enaknya React adalah begitu set state langsung component-nya ter-update. Mari simak contoh kode di bawah ini.

import React, { useState } from 'react';

const TodoList = () => {
  const [todos, setTodos] = useState([]);

  const addTodo = (item) => {
    const oldTodos = todos;
    const newTodos = oldTodos.concat(item);

    setTodos(newTodos);
  }

  const removeTodo = (index) => {
    const oldTodos = todos;
    const newTodos = oldTodos.filter((item, i) => i !== index);

    setTodos(newTodos);
  }

  return (
    <div>
      <ul>
        {
          todos.map((item, index) => (
            <li key={index}>
              <span>{item}</span>
              <button onClick={() => removeTodo(index)}>
                X
              </button>
            </li>
          ))
        }
      </ul>
      <form onSubmit={(event) => {
        event.preventDefault();
        addTodo(event.currentTarget.item.value);
        event.currentTarget.item.value = '';
      }}>
        <input type='text' name='item' required />
        <button>add</button>
      </form>
    </div>
  );
}

export default TodoList;

Mudah dimengerti kan kodenya? Simpel kan? Itu bagus. Kode yang simpel, mudah dimengerti, straightforward, dan tidak bertele-tele adalah hal yang bagus.

Sekarang, ternyata data todos itu akan digunakan juga di component lain. Berarti state-nya tak bisa disimpan di dalam component ini. Berangkatlah kita menuju membuat state ini menjadi global state. Di saat seperti ini, tak ayal kita langsung berpikiran ke metode reducer semacam Redux maupun useReducer. Tapi, metode reducer itu adalah sebuah paradigma pemrograman tersendiri yang berbeda dengan metode simpel kita di atas. Jikalau tujuannya hanya untuk membuat global state apakah harus mengganti paradigma? Atau bisa dengan restrukturisasi saja? Di tulisan ini saya akan menjelaskan bagaimana restrukturisasi agar bisa membuat global state.

Tujuan

  • State-nya global dan tetap auto re-render.
  • Tetap simpel, mudah dimengerti, straightforward, dan tidak bertele-tele.

Dari sisi component, tujuan hasil akhirnya adalah seperti ini.

import React from 'react';
import {useTodos, addTodo, removeTodo} from './todos';

const TodoList = () => {
  const todos = useTodos();

  return (
    <div>
      <ul>
        {
          todos.map((item, index) => (
            <li key={index}>
              <span>{item}</span>
              <button onClick={() => removeTodo(index)}>
                X
              </button>
            </li>
          ))
        }
      </ul>
      <form onSubmit={(event) => {
        event.preventDefault();
        addTodo(event.currentTarget.item.value);
        event.currentTarget.item.value = '';
      }}>
        <input type='text' name='item' required />
        <button>add</button>
      </form>
    </div>
  );
}

export default TodoList;

Masih sama kan ya dengan kode yang awal. Kita hanya memindahkan akses terhadap array todos menjadi kepada sebuah hook, dan juga memindahkan fungsi-fungsi pengubah isi todos ke sebuah tempat tersendiri yang bisa di-import siapapun sehingga menjadi sesuatu yang global. Karena masih sama begini, berarti tujuan kita untuk menjaga agar kode kita tetap simpel, mudah dimengerti, straightforward, dan tidak bertele-tele menjadi tercapai.

todos.js

Sekarang mari kita buat isinya.

let todos = [];

export const addTodo = (item) => {
  const oldTodos = todos;
  const newTodos = oldTodos.concat(item);

  todos = newTodos;
}

export const removeTodo = (index) => {
  const oldTodos = todos;
  const newTodos = oldTodos.filter((item, i) => i !== index);

  todos = newTodos;
}

export const useTodos = () => {
  // to be implemented
}

Sama kan isinya dengan kode kita yang awal, masih menjaga tujuan kita. Tinggal satu hal yang belum, yaitu state-nya bisa auto re-render. Assignment ke variable todos di situ tak akan me-render ulang component-nya. Mari kita buat agar bisa auto re-render, dan itu kuncinya ada di fungsi useTodos.

useState + EventEmitter

Pertama-tama, kita perlu bisa notice terlebih dahulu ketika ada perubahan pada todos. Sesuatu harus me-notify kepada sesuatu agar sesuatu tersebut menjadi ter-notify. Familiar sekali kebutuhan ini. Salah satunya adalah dengan cara emit event. Seorang listener me-listen ke sebuah event sehingga ketika event tersebut di-emit, listener tersebut dipanggil. Sama kan? Sepadan kan? Oleh karena itu, kita akan menggunakan metode emit event ini, yaitu dengan menggunakan library events yang merupakan implementasi multi-platform dari standard events di NodeJS.

import EventEmitter from 'events';

const EVENT = 'TODOS_CHANGED';
let todos = [];
const emitter = new EventEmitter();

export const addTodo = (item) => {
  const oldTodos = todos;
  const newTodos = oldTodos.concat(item);

  todos = newTodos;
  emitter.emit(EVENT, newTodos, oldTodos);
}

export const removeTodo = (index) => {
  const oldTodos = todos;
  const newTodos = oldTodos.filter((item, i) => i !== index);

  todos = newTodos;
  emitter.emit(EVENT, newTodos, oldTodos);
}

export const useTodos = () => {
  // to be implemented
}

Sip? Bisa dimengerti kan? Jadi, ketika melakukan add dan remove todo, kita meng-emit event TODOS_CHANGED dengan menyertakan state yang baru dan yang lamanya. Setelah kita me-notify perubahan ini, lalu sekarang saatnya membuat sesuatu yang akan ter-notify-kan oleh event ini, yaitu di dalam useTodos.

useTodos

import {useState, useEffect} from 'react';

// ...

export const useTodos = () => {
  const [value, setValue] = useState(todos);

  useEffect(() => {
    const listener = (newTodos, oldTodos) => setValue(newTodos);
    emitter.on(EVENT, listener);
    return () => emitter.removeListener(EVENT, listener);
  }, []);

  return value;
}

Intinya, useTodos ini me-listen terhadap event TODOS_CHANGED yang akan di-emit saat melakukan add dan remove todo, lalu dia men-set sebuah state di dalam dirinya yang adalah array todos. Sehingga daripada itu, component yang menggunakan hook ini akan auto re-render.

Recap

// TodoList.js

import React from 'react';
import {useTodos, addTodo, removeTodo} from './todos';

const TodoList = () => {
  const todos = useTodos();

  return (
    <div>
      <ul>
        {
          todos.map((item, index) => (
            <li key={index}>
              <span>{item}</span>
              <button onClick={() => removeTodo(index)}>
                X
              </button>
            </li>
          ))
        }
      </ul>
      <form onSubmit={(event) => {
        event.preventDefault();
        addTodo(event.currentTarget.item.value);
        event.currentTarget.item.value = '';
      }}>
        <input type='text' name='item' required />
        <button>add</button>
      </form>
    </div>
  );
}

export default TodoList;
// todos.js

import {useState, useEffect} from 'react';
import EventEmitter from 'events';

const EVENT = 'TODOS_CHANGED';
let todos = [];
const emitter = new EventEmitter();

export const addTodo = (item) => {
  const oldTodos = todos;
  const newTodos = oldTodos.concat(item);

  todos = newTodos;
  emitter.emit(EVENT, newTodos, oldTodos);
}

export const removeTodo = (index) => {
  const oldTodos = todos;
  const newTodos = oldTodos.filter((item, i) => i !== index);

  todos = newTodos;
  emitter.emit(EVENT, newTodos, oldTodos);
}

export const useTodos = () => {
  const [value, setValue] = useState(todos);

  useEffect(() => {
    const listener = (newTodos, oldTodos) => setValue(newTodos);
    emitter.on(EVENT, listener);
    return () => emitter.removeListener(EVENT, listener);
  }, []);

  return value;
}

Kode ini just plain JavaScript array and functions beserta sebuah hook. Karena tak ada paradigma baru yang dibawa di sini, maka kode ini (arguably) lebih mudah dimengerti bahkan oleh orang awam. Juga, karena just plain JavaScript functions, jadi (arguably) tak repot juga untuk melakukan unit test-nya. Selain itu, kode ini sangat straightforward, minim dependensi, dan just plain JavaScript functions sehingga (arguably) mempermudah dalam bugfixing.

Dapat dibaca bahwa poin-poin saya di atas semuanya "arguably" karena setiap programmer punya kebiasaan dan kenyamanan masing-masing dalam memrogram. Tidak pula saya bilang kita semua harus melakukannya seperti cara ini karena setiap programmer punya kebutuhan yang berbeda-beda. Di sini saya cuman sekedar sharing perspektif lain dan semoga ini bisa mencerahkan kita semua. Karena saya gatal kalau menemui kasus yang sebagai berikut.

  • "Kenapa pakai Redux?"
  • "Supaya state-nya bisa dipakai di component lain"
  • "Lalu alasan apa lagi selain itu?"
  • "Ya, itu, supaya jadi global state"