Belajar Redux - Security & Best Practice State
Episode 15 of 23

Belajar Redux - Security & Best Practice State

Episode ini membahas keamanan dan praktik terbaik state management: kenapa token sebaiknya tidak masuk store, cara sanitasi data API, proteksi akses berbasis role, prinsip state minimal yang benar-benar global, dan menaruh server state di RTK Query.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Pendahuluan

State management bukan hanya soal kecepatan dan struktur — ia juga menyangkut keamanan. Apa pun yang kalian simpan di store Redux, dengan Redux DevTools yang terpasang, akan bisa dibaca oleh siapa saja yang membuka konsol browser. Episode 15 membahas garis batas mana data yang aman masuk store dan mana yang sebaiknya tinggal di tempat lain.

Kita akan membahas penanganan token dan data sensitif, sanitasi data yang datang dari API, proteksi akses berbasis role, lalu prinsip state minimal: hanya data yang benar-benar global dan dibutuhkan banyak komponen yang layak tinggal di Redux, sementara server state ditangani RTK Query.

Data Sensitif di dalam Store

Kenapa Token Jangan Disimpan di Store

Redux DevTools me-render seluruh state tree. Menyimpan JWT di store berarti token dapat dilihat, disalin, bahkan di-export oleh siapa pun yang memiliki akses ke tab DevTools. Bahaya bertambah bila aplikasi di-hydrate dari server: token ikut terkirim di HTML dan terlihat di view-source.

State yang BURUK untuk DevTools
{
  "auth": {
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiJ9...",
    "refreshToken": "r_9f2c1a"
  }
}

Larangan utama: jangan pernah menyimpan refreshToken di store — sekali bocor, attacker bisa memperbarui sesi selamanya. Token akses pun hanya boleh tinggal di memory bila benar-benar perlu dibaca oleh reducer.

Alternatif Penyimpanan Token

Token akses yang hanya dipakai sebagai header request sebaiknya disimpan di luar Redux:

JSModul token terpisah dari store
let accessToken: string | null = null
 
export function setAccessToken(token: string | null) {
  accessToken = token
}
 
export function getAccessToken() {
  return accessToken
}

Modul memory di atas tidak pernah terlihat di Redux DevTools dan di-reset saat halaman ditutup. Untuk persistensi antar reload, gunakan cookie httpOnly yang dikelola server — bukan localStorage yang bisa dibaca script XSS.

Sanitasi Data API

Jangan Menyalin Semua yang Dikirim Server

Respons API sering membawa field internal yang tidak dibutuhkan UI: internalNotes, debugFlag, adminPanelUrl. Menyimpan semuanya ke store membuka risiko informasi bocor ke konsol. Bentuk ulang data sebelum masuk state:

JSMemetakan data API sebelum disimpan
const toPublicUser = (raw) => ({
  id: raw.id,
  name: raw.name,
  email: raw.email,
  role: raw.role,
})
 
export const fetchUsers = createAsyncThunk("users/fetchUsers", async () => {
  const res = await fetch("/api/users")
  const raw = await res.json()
  return raw.map(toPublicUser)
})

toPublicUser hanya memilih field yang memang ditampilkan UI. Sanitasi seperti ini juga menjaga ukuran state tetap kecil dan mencegah konsumen data lain membaca field yang tidak seharusnya mereka lihat.

Validasi di Batas Masuk

Tegakkan bentuk data sejak awal dengan TypeScript dan, bila perlu, runtime validation:

JSValidasi bentuk data API
interface SafeUser {
  id: number
  name: string
  email: string
  role: "admin" | "editor" | "viewer"
}
 
const isSafeUser = (u): u is SafeUser =>
  typeof u.id === "number" &&
  typeof u.name === "string" &&
  typeof u.email === "string" &&
  ["admin", "editor", "viewer"].includes(u.role)

isSafeUser adalah type guard yang memvalidasi bentuk data saat runtime. Kalian bisa mengombinasikannya dengan zod atau io-ts untuk aplikasi yang butuh jaminan lebih ketat.

Proteksi Akses Berbasis Role

State Role untuk Gate Komponen

Perlindungan utama selalu di server, tetapi state role membantu UI menampilkan atau menyembunyikan aksi sesuai hak akses. Simpan role hasil otorisasi server, lalu buat selector:

JSSelector role-aware
import { createSelector } from "@reduxjs/toolkit"
 
const selectAuth = (state) => state.auth
 
export const selectCurrentRole = createSelector(
  [selectAuth],
  (auth) => auth.user?.role ?? null,
)
 
export const selectCanEdit = createSelector(
  [selectCurrentRole],
  (role) => role === "admin" || role === "editor",
)

Komponen memakai selectCanEdit untuk menampilkan tombol edit. Penting diingat: ini hanya kenyamanan UI, bukan otorisasi. Request ke server tetap harus divalidasi kembali dengan hak akses server-side.

Role Tidak Pernah dari Client

Jangan pernah mempercayai role yang dikirim client. Ambil role dari klaim token yang diverifikasi server atau dari endpoint /api/me:

JSMengambil role dari server
export const fetchMe = createAsyncThunk("auth/fetchMe", async () => {
  const res = await fetch("/api/me", {
    headers: { Authorization: `Bearer ${getAccessToken()}` },
  })
  return res.json()
})

fetchMe menentukan role dari respons server. Store hanya menampung hasil otorisasi, tidak pernah menjadi sumber kebenaran keamanan.

Prinsip State Minimal

Yang Benar-benar Perlu Global

Sebelum menambahkan slice, tanyakan: apakah data ini dibutuhkan banyak komponen di berbagai tempat? Jika hanya dipakai satu subtree, simpan di state lokal komponen. Aturan praktisnya:

Tiga pertanyaan sebelum menambah global state
1. Apakah data ini dibaca oleh banyak komponen berbeda?
2. Apakah data ini berubah di banyak tempat dan harus sinkron?
3. Apakah data ini perlu dilacak untuk debugging lintas waktu?

Jawab ya untuk ketiganya? Masukkan ke Redux. Jika tidak, biarkan di state lokal atau context kecil. Store yang terlalu gemuk lebih lambat di-debug dan lebih sering memicu re-render.

Server State di RTK Query

Server state — data yang datang dari API dan di-cache — bukan tanggung jawab slice manual. Taruh di RTK Query seperti episode 8 dan 9 agar caching, invalidasi, dan re-fetch berjalan otomatis:

JSServer state dengan RTK Query
export const notificationsApi = createApi({
  reducerPath: "notificationsApi",
  baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
  endpoints: (builder) => ({
    getNotifications: builder.query<Notification[], void>({
      query: () => "notifications",
    }),
  }),
})

Hasilnya, slice manual hanya fokus pada state UI dan client-only. Data yang bisa berubah di banyak sumber ditangani sistem yang sudah memahami siklus hidupnya.

Warning

Seimbangkan jumlah slice per fitur. Satu slice raksasa yang menampung banyak domain menyulitkan debugging; sepuluh slice mikro untuk satu layar justru menambah overhead. Satu slice per domain yang jelas adalah titik tengah yang sehat.

Penutup

Keamanan state dimulai dari keputusan apa yang boleh masuk store. Token dan refresh token dijauhkan dari Redux DevTools, data API disanitasi sebelum disimpan, dan role hanya mengatur tampilan — bukan otorisasi. Best practice melengkapi: state Redux hanya untuk data yang benar-benar global, server state diserahkan ke RTK Query, dan jumlah slice dijaga seimbang per fitur.

Inti yang harus dibawa pulang:

  • Jangan simpan refreshToken di store; token akses pun hanya bila benar-benar perlu.
  • Simpan token di modul memory atau cookie httpOnly, bukan localStorage.
  • Sanitasi respons API: simpan hanya field yang dibutuhkan UI.
  • Proteksi akses tetap di server; role di store hanya untuk keputusan tampilan.
  • Hanya data yang dibutuhkan banyak komponen yang layak menjadi global state.
  • Taruh server state di RTK Query dan jaga keseimbangan jumlah slice per fitur.

Episode 16 berikutnya membahas custom middleware & enhancers — kalian akan memahami struktur middleware, menulis custom middleware untuk logging dan telemetri, serta memperluas store dengan enhancers dan komposisi middleware default RTK.

Belajar Redux - Security & Best Practice State | Belajar Redux