Mengelola State Aplikasi React Native dengan Zustand: Persistensi dan Sinkronisasi Data Offline
Membentuk Store Minimal dengan create dan set
create dari zustand mengembalikan hook yang menyimpan state di luar React tree. Setiap komponen yang memanggil hook tersebut membaca snapshot yang sama, sehingga tidak diperlukan Context Provider untuk berbagi data. Hook tersebut juga membawa getState, setState, dan subscribe untuk mengendalikan store dari luar komponen.
Bentuk store dimulai dari state awal dan kumpulan action. Action addTask menambahkan entri baru, toggleTask membalik status done, dan clearCompleted menghapus task yang sudah selesai. Semua action ditulis sebagai fungsi biasa di dalam store, bukan di dalam komponen.
set menerima partial state atau function (state) => partial. Bentuk function dipakai saat update bergantung pada nilai terbaru, misalnya menambah task ke array yang sedang berubah. get membaca state saat action berjalan tanpa membuat komponen berlangganan perubahan.
Array baru dibuat di setiap update agar referensi berubah dan selector mendeteksi perbedaan. Mutasi langsung pada state.tasks membuat zustand tidak melihat perubahan tersebut.
Pemisahan ini membuat store dapat dijalankan langsung di Node. createStore dari zustand/vanilla menghasilkan API yang sama dengan create tanpa lapisan React, sehingga logika state bisa diuji lewat terminal.

Gambar: Siklus aliran data satu arah pada store — komponen (View) memicu action, action mengubah state di store di luar React tree, lalu state baru dibaca kembali oleh view untuk render ulang. — Sumber: Redux Docs
// npm install zustand
import { createStore } from 'zustand/vanilla';
const useTodoStore = createStore((set, get) => ({
tasks: [
{ id: 1, title: 'Review pull request', done: false },
{ id: 2, title: 'Tulis dokumentasi store', done: true },
],
addTask: (title) =>
set((state) => ({
tasks: [
...state.tasks,
{
id: Math.max(0, ...state.tasks.map((task) => task.id)) + 1,
title,
done: false,
},
],
})),
toggleTask: (id) =>
set((state) => ({
tasks: state.tasks.map((task) =>
task.id === id ? { ...task, done: !task.done } : task
),
})),
clearCompleted: () => {
set({ tasks: get().tasks.filter((task) => !task.done) });
},
}));
const { addTask, toggleTask, clearCompleted } = useTodoStore.getState();
addTask('Simulasikan mode offline');
toggleTask(1);
clearCompleted();
console.log('task tersisa:', useTodoStore.getState().tasks);Output:
task tersisa: [ { id: 3, title: 'Simulasikan mode offline', done: false } ]Output yang muncul adalah satu task tersisa setelah toggleTask dan clearCompleted bekerja. Task pertama berubah menjadi selesai, lalu dihapus, sementara task baru tetap pending. Pola ini memperlihatkan bahwa action dapat dirangkai tanpa render komponen.
Memilih Bagian State Tanpa Re-render Berlebih
Selector useStore((s) => s.tasks) mengembalikan referensi yang stabil selama irisan state tidak berubah. Komponen hanya render ulang ketika referensi tersebut berubah, bukan setiap kali field lain di store diperbarui.
Selector yang membuat objek baru setiap pemanggilan merusak stabilitas tersebut. Bentuk (s) => ({ tasks: s.tasks }) menghasilkan objek berbeda di setiap render, sehingga zustand menganggap state berubah dan memicu render berulang.
useShallow dari zustand/react/shallow menangani selector yang memang menghasilkan objek turunan. Hook tersebut membandingkan isi objek secara dangkal, sehingga referensi baru dengan nilai sama tidak memicu render tambahan.
Pemisahan store per domain membantu menjaga permukaan state tetap kecil. authStore dan cartStore yang terpisah membuat perubahan pada salah satunya tidak menyentuh komponen yang hanya memakai domain lain.
Action yang didefinisikan di dalam store juga stabil secara referensi. Komponen anak tidak memerlukan useCallback tambahan untuk menerima action sebagai prop, karena fungsi tersebut tidak dibuat ulang saat render.
Selector dapat dipanggil di luar komponen lewat useStore.getState(). Pola ini berguna di event handler atau fungsi utilitas yang perlu membaca state tanpa berlangganan perubahan.
Menyimpan State ke Storage dengan Middleware persist
persist membungkus store dan menulis state ke storage setiap kali terjadi perubahan. Middleware ini menangani proses serialisasi dan rehidrasi, sehingga store tidak perlu memanggil storage secara manual.
partialize membatasi field yang disimpan. Field sementara seperti isSyncing sebaiknya dikecualikan karena nilainya bergantung pada sesi berjalan dan tidak bermakna setelah aplikasi dibuka ulang.
Advanced Architecture with Laravel Containers & Queues
A problem-driven, project-based course designed to elevate intermediate develope...
Middleware menulis state secara asinkron, sehingga update beruntun dapat menghasilkan beberapa operasi storage. Untuk data besar, batasi field dengan partialize dan hindari menyimpan hasil turunan yang bisa dihitung ulang.
Adapter storage cukup menyediakan getItem, setItem, dan removeItem. Di React Native, adapter tersebut adalah AsyncStorage. Di Node, adapter dapat berupa file JSON lokal dengan bentuk API yang sama.
createJSONStorage menangani serialisasi, sehingga adapter hanya menerima dan mengembalikan string. Kita tidak perlu memanggil JSON.stringify di dalam adapter.
// Ilustrasi untuk React Native, tidak dijalankan di Node
import AsyncStorage from '@react-native-async-storage/async-storage';
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
type Task = { id: number; title: string; done: boolean };
type TaskState = {
tasks: Task[];
isSyncing: boolean;
addTask: (title: string) => void;
};
export const useTaskStore = create<TaskState>()(
persist(
(set) => ({
tasks: [],
isSyncing: false,
addTask: (title) =>
set((state) => ({
tasks: [
...state.tasks,
{ id: state.tasks.length + 1, title, done: false },
],
})),
}),
{
name: 'task-storage',
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({ tasks: state.tasks }),
}
)
);Pada contoh di atas, partialize hanya menyimpan tasks. Field isSyncing tetap berada di memory dan kembali ke nilai default setelah rehydrate. Adapter file di bawah memakai kontrak yang sama dengan AsyncStorage, sehingga perilakunya dapat diamati tanpa perangkat mobile.
// npm install zustand
import { createStore } from 'zustand/vanilla';
import { persist, createJSONStorage } from 'zustand/middleware';
import { readFile, writeFile, rm } from 'node:fs/promises';
const STORAGE_KEY = 'todo-store';
const fileStorage = {
getItem: async (name) => {
try {
return await readFile(`${name}.json`, 'utf8');
} catch {
return null;
}
},
setItem: async (name, value) => {
await writeFile(`${name}.json`, value, 'utf8');
},
removeItem: async (name) => {
await rm(`${name}.json`, { force: true });
},
};
function createTodoStore() {
return createStore(
persist(
(set) => ({
tasks: [],
isSyncing: false,
addTask: (title) =>
set((state) => ({
tasks: [
...state.tasks,
{ id: state.tasks.length + 1, title, done: false },
],
})),
setSyncing: (isSyncing) => set({ isSyncing }),
}),
{
name: STORAGE_KEY,
storage: createJSONStorage(() => fileStorage),
partialize: (state) => ({ tasks: state.tasks }),
skipHydration: true,
}
)
);
}
const firstSession = createTodoStore();
firstSession.getState().addTask('Sinkronisasi antrian offline');
firstSession.getState().setSyncing(true);
await new Promise((resolve) => setTimeout(resolve, 100));
const raw = JSON.parse(await readFile(`${STORAGE_KEY}.json`, 'utf8'));
console.log('isi file storage:', JSON.stringify(raw));
console.log('isSyncing tersimpan?', 'isSyncing' in raw.state);
const nextSession = createTodoStore();
await nextSession.persist.rehydrate();
console.log('tasks setelah rehydrate:', nextSession.getState().tasks);
console.log('isSyncing setelah rehydrate:', nextSession.getState().isSyncing);Output:
isi file storage: {"state":{"tasks":[{"id":1,"title":"Sinkronisasi antrian offline","done":false}]},"version":0}
isSyncing tersimpan? false
tasks setelah rehydrate: [ { id: 1, title: 'Sinkronisasi antrian offline', done: false } ]
isSyncing setelah rehydrate: falseIsi file todo-store.json menunjukkan bentuk { state, version } yang ditulis middleware. Field isSyncing tidak muncul di file. Store kedua yang dibuat dari factory yang sama berhasil memulihkan tasks, sementara isSyncing tetap false.
Mengendalikan Hydration dan Perubahan Skema Data
Hydration berjalan asinkron karena pembacaan storage membutuhkan waktu. onRehydrateStorage menyediakan callback untuk menandai kapan proses selesai, misalnya dengan mengubah flag hasHydrated di store. Layar yang bergantung pada token sebaiknya menahan render sampai flag tersebut bernilai true.
skipHydration dipakai saat hydration harus dipicu manual. Skenario umum adalah menunggu sesi login siap, lalu memanggil store.persist.rehydrate() agar data pengguna yang benar yang dimuat.
version dan migrate menangani perubahan bentuk data. Saat field baru ditambahkan atau nama field berubah, migrate menerima state versi lama dan mengembalikan bentuk yang sesuai dengan versi terbaru. Instalasi pengguna lama tidak perlu dihapus.
merge menggabungkan state tersimpan dengan default terbaru. Fungsi ini berguna ketika store menambah field baru yang belum ada di storage. Tanpa merge, field tersebut dapat hilang setelah rehydrate.
onFinishHydration dapat dipakai untuk menjalankan efek setelah state siap, misalnya memicu flush antrian atau menyegarkan data dari server.
Risiko utama adalah menampilkan data lama sebelum hydration selesai. Layar yang membaca token dari storage dapat menganggap pengguna belum login, lalu mengalihkan navigasi ke halaman yang salah.
Membangun Antrian Sinkronisasi Saat Offline
Setiap mutasi lokal dicatat sebagai operasi pending dengan id, type, payload, dan updatedAt. id berfungsi ganda sebagai idempotency key, sehingga server dapat mengabaikan operasi yang sama ketika retry terjadi.
flushQueue mengirim operasi satu per satu dan menghapus entri hanya setelah server mengonfirmasi. Operasi yang gagal tetap berada di antrian dengan penghitung attempts yang bertambah. Antrian sebaiknya ikut dipersistenkan agar operasi tidak hilang saat aplikasi ditutup.
Retry memakai backoff sederhana dan batas percobaan. Batas ini mencegah antrian tumbuh tanpa kendali saat server mengalami gangguan berkepanjangan. Di React Native, status koneksi dibaca dari @react-native-community/netinfo sebelum flush dijalankan.
Resolusi konflik membutuhkan aturan yang eksplisit. Strategi last-write-wins memakai updatedAt sebagai pembanding, sedangkan data yang lebih sensitif sebaiknya ditandai sebagai konflik untuk ditinjau pengguna.

Gambar: Alur write-queue pada arsitektur offline-first — perubahan ditulis ke data lokal lebih dulu, operasi dimasukkan ke antrean jaringan, konflik diselesaikan, lalu entri dihapus setelah server mengonfirmasi. — Sumber: Android Developers
// npm install zustand
import { createStore } from 'zustand/vanilla';
const fakeServer = {
seen: new Map(),
async push(operation) {
const attempts = (this.seen.get(operation.id) ?? 0) + 1;
this.seen.set(operation.id, attempts);
if (attempts < 3) {
throw new Error('network unavailable');
}
return { id: operation.id, acceptedAt: Date.now() };
},
};
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const useSyncStore = createStore((set) => ({
online: true,
queue: [],
enqueue: (type, payload) =>
set((state) => ({
queue: [
...state.queue,
{
id: `op-${state.queue.length + 1}-${Date.now()}`,
type,
payload,
updatedAt: Date.now(),
attempts: 0,
},
],
})),
markAttempt: (id) =>
set((state) => ({
queue: state.queue.map((operation) =>
operation.id === id
? { ...operation, attempts: operation.attempts + 1 }
: operation
),
})),
remove: (id) =>
set((state) => ({
queue: state.queue.filter((operation) => operation.id !== id),
})),
}));
async function flushQueue(store, server, maxAttempts = 3) {
if (!store.getState().online) {
console.log('offline, flush ditunda');
return;
}
for (const operation of [...store.getState().queue]) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await server.push(operation);
store.getState().remove(operation.id);
console.log(`success ${operation.type} pada percobaan ${attempt}`);
break;
} catch (error) {
store.getState().markAttempt(operation.id);
if (attempt === maxAttempts) {
console.log(`gagal permanen ${operation.type}: ${error.message}`);
break;
}
console.log(
`retry ${operation.type} percobaan ${attempt}: ${error.message}`
);
await wait(50 * attempt);
}
}
}
}
const store = useSyncStore;
store.getState().enqueue('task.create', { title: 'Catatan offline' });
store.getState().enqueue('task.toggle', { id: 1, done: true });
console.log('pending sebelum flush:', store.getState().queue.length);
await flushQueue(store, fakeServer);
console.log('pending setelah flush:', store.getState().queue.length);Output:
pending sebelum flush: 2
retry task.create percobaan 1: network unavailable
retry task.create percobaan 2: network unavailable
success task.create pada percobaan 3
retry task.toggle percobaan 1: network unavailable
retry task.toggle percobaan 2: network unavailable
success task.toggle pada percobaan 3
pending setelah flush: 0Server palsu di atas gagal dua kali untuk setiap operasi, lalu menerima percobaan ketiga. Log menunjukkan urutan retry, retry, dan success untuk kedua operasi. Setelah flush selesai, panjang antrian menjadi nol karena entri hanya dihapus setelah konfirmasi.
Menguji Store dan Mengawasi Perilakunya
Pengujian action dapat dilakukan lewat store.getState() tanpa merender komponen. Pola ini membuat test berjalan cepat dan tidak bergantung pada environment React Native.
State perlu direset antar test dengan useStore.setState(initialState, true). Argumen kedua mengganti seluruh state, sehingga sisa data dari test sebelumnya tidak mencemari test berikutnya.
Isi storage setelah persist juga layak diperiksa. Test dapat membaca file atau memanggil adapter secara langsung untuk memastikan partialize benar-benar membuang field yang tidak dibutuhkan.
subscribe membantu melacak mutasi yang tidak terduga. Listener mencatat state sebelum dan sesudah perubahan, sehingga action yang memicu update berlebihan dapat ditemukan lebih cepat. Log ini juga memperlihatkan selector mana yang ikut render saat satu field berubah.
Data sensitif seperti token sebaiknya tidak disimpan tanpa enkripsi. AsyncStorage bukan tempat penyimpanan terenkripsi. Untuk kredensial, gunakan react-native-keychain atau expo-secure-store, dan simpan hanya penanda sesi di store.
Untuk test otomatis, adapter storage dapat diganti dengan objek in-memory. Penggantian ini membuat test tidak menyentuh disk dan tetap menguji alur persist secara utuh.
Membangun aplikasi offline-first membutuhkan pemahaman state, persistensi, dan sinkronisasi yang saling terhubung. Rumah Coding menyediakan kelas React Native dan mobile state management yang membahas pola ini secara terstruktur, mulai dari store minimal hingga antrian sinkronisasi. Kita dapat mempelajari implementasi langsung dengan studi kasus aplikasi nyata dan review kode bersama mentor.
Kursus Terkait
Advanced Architecture with Laravel Containers & Queues
A problem-driven, project-based course designed to elevate intermediate developers to a senior architectural mindset. Instead of just memorizing documentation, students will tackle real-world bottleneck issues by mastering the Inversion of Control (IoC) principle, Dependency Injection, and asynchronous background processing. Learn to decouple services and orchestrate robust queues to build scalable, high-performance applications that never freeze under heavy loads.
Asynchronous Bulk E-Certificate & Notification Engine
- Decoupled Service Architecture: PDF generation and Email delivery are built as independent, interface-driven services injected via the Service Container.
- Job Chaining: Ensuring processes run in strict asynchronous order (Generate PDF ➔ Upload to Storage ➔ Send Email).
- Job Batching & Real-Time Tracking: Grouping hundreds of jobs together and displaying a live progress bar (e.g., "65% Completed") on the admin dashboard.
Advanced Flutter State Management with BLoC
Master advanced Flutter state management by building a production-ready application. This intermediate course uses a top-down, problem-driven approach, plunging you into real-world engineering challenges. You will learn to architect scalable applications, handle complex reactive states, manage multi-BLoC communication, synchronize real-time data, and implement optimistic UI updates using industry-standard BLoC patterns.
TaskSync: Real-Time Collaborative Task Manager
- Role-Based Authentication: Secure login and session management, dynamically reflecting user states across the entire application.
- Real-Time Task Board: A Kanban-style board that instantly updates across all devices when any team member creates, moves, or deletes a task.
- Advanced Search & Filtering: High-performance local search with event debouncing to prevent unnecessary API calls.
API Development with Golang
A hands-on, project-based course designed to teach beginners how to build robust, high-performance RESTful APIs using Golang. Starting from core Go syntax and concurrency, students will progress through routing, database integration, security, and finally containerize their application for production deployment.
EduTrack: Learning Management & Assessment API
- Role-Based Access Control (RBAC)
- Course & Module Management
- Enrollment & Progress Tracking