Episode ini membahas pola lanjutan dan manajemen state: state di client dengan React hooks, shared state dan server-driven UI, progressive enhancement dengan optimistic updates, serta data jangka panjang dengan revalidasi di latar belakang.

Kalian sudah punya aplikasi yang berfungsi dan cepat. Sekarang waktunya membuatnya terasa hidup: state yang berinteraksi mulus, UI yang merespons seketika, dan data yang tetap segar tanpa usaha manual. Inilah dunia advanced patterns dan state di Remix.
Filosofi Remix menempatkan state dengan disiplin: server adalah sumber kebenaran, dan client menampilkan apa yang dikirim server. Sebagian besar masalah state yang rumit di SPA justru menghilang — kalian tidak perlu sinkronisasi dua arah yang rawan salah, karena Remix sudah merevalidasi data secara otomatis.
Episode 15 akan membahas state lokal dengan hooks, shared state dan server-driven UI, optimistic updates, serta revalidasi data jangka panjang.
Untuk interaksi murni lokal — toggle, tab, dropdown — state React biasa sudah cukup. useState, useReducer, dan useCallback menjaga logika tetap dekat dengan komponennya. State lokal sebaiknya untuk UI ephemeral yang tidak butuh server.
import { useReducer } from "react";
function reducer(state, aksi) {
switch (aksi.tipe) {
case "tambah": return { jumlah: state.jumlah + 1 };
case "kurang": return { jumlah: state.jumlah - 1 };
default: return state;
}
}
export default function Penghitung() {
const [state, dispatch] = useReducer(reducer, { jumlah: 0 });
return (
<div>
<p>Jumlah: {state.jumlah}</p>
<button onClick={() => dispatch({ tipe: "tambah" })}>Tambah</button>
</div>
);
}useReducer menampung logika state yang mulai kompleks dalam satu fungsi murni. Untuk counter dan kasus serupa, ini lebih mudah diuji daripada banyak useState.
Aturan sederhana: jika state hanya dipakai satu komponen, biarkan di komponen itu. Begitu dua komponen butuh data yang sama, pertimbangkan mengangkatnya ke parent. Jika data itu berasal dari server, jangan duplikasi — biarkan loader yang memegangnya.
Data yang dipakai banyak route sebaiknya di-load di route induk. Dengan nested routes, induk me-render Outlet dan anak-anaknya — useLoaderData di induk memberi akses ke data yang sama di semua anak tanpa duplikasi. Ini adalah bentuk shared state bawaan Remix.
Filosofi ini membalik arah: bukan client yang meminta data lalu memutuskan UI, melainkan server yang mengirim data dan client yang menampilkan sesuai data itu. Loader yang mengembalikan data dan komponen yang me-render berdasar data disebut server-driven UI. Hasilnya: aturan tampilan konsisten dan mudah diuji.
export async function loader() {
return { status: "pending", pesan: "Menunggu moderasi" };
}
export default function StatusKonten() {
const data = useLoaderData();
if (data.status === "pending") {
return <p>Konten menunggu peninjauan.</p>;
}
return <p>Konten aktif.</p>;
}Optimistic update menampilkan hasil seolah-olah sudah berhasil sebelum server mengonfirmasi. Di Remix, useFetcher memungkinkan ini: tampilkan state baru langsung, kirim ke action, lalu biarkan revalidasi mengonfirmasi hasil sebenarnya.
import { useFetcher } from "@remix-run/react";
export default function TombolSuka({ postId, sudahSuka }) {
const fetcher = useFetcher();
const suka = fetcher.formData
? fetcher.formData.get("suka") === "ya"
: sudahSuka;
return (
<fetcher.Form method="post">
<input type="hidden" name="postId" value={postId} />
<input type="hidden" name="suka" value={suka ? "tidak" : "ya"} />
<button type="submit">{suka ? "Batal suka" : "Suka"}</button>
</fetcher.Form>
);
}fetcher.formData berisi data form yang sedang dikirim — basis untuk tampilan optimis. Saat revalidasi selesai, state resmi menggantikan tampilan sementara.
Optimistic update aman untuk aksi yang hampir pasti berhasil dan mudah dibatalkan: suka, baca, perbarui status. Untuk pembayaran atau hapus permanen, tunggu konfirmasi server — kesalahan di sana jauh lebih mahal.
Remix merevalidasi loader secara otomatis setelah action selesai. Ini menutup lingkaran data: apa pun yang diubah action, UI langsung menampilkan hasil terbaru. Kalian tidak menulis kode refetch manual; Remix yang memutuskan kapan data harus di-refresh.
Untuk data yang berubah dari luar — notifikasi, harga, status job — gunakan interval atau event. Revalidasi manual bisa dipicu memakai useRevalidator dari Remix. useRevalidator memberi kontrol untuk memanggil ulang loader tanpa navigasi.
Untuk kebutuhan real-time yang sesungguhnya — chat atau kolaborasi — pertimbangkan WebSocket atau layanan push terpisah. Remix menyelesaikan sebagian besar kasus, tetapi data yang harus muncul dalam hitungan detik punya solusi khusus sendiri.
Episode 15 menyelesaikan puzzle state: state lokal dengan hooks untuk interaksi, shared state via route induk dan server-driven UI, optimistic updates dengan useFetcher, serta revalidasi otomatis dan manual untuk data jangka panjang. State kini tidak lagi menakutkan.
Inti yang harus dibawa pulang:
Di episode 16 selanjutnya kita akan membahas testing dan quality — unit testing komponen React, integration testing untuk loader dan action, E2E testing dengan Playwright atau Cypress, serta accessibility testing dan audit kualitas. State sudah hidup; sekarang pastikan tidak ada yang rusak.