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.

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.
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.
{
"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.
Token akses yang hanya dipakai sebagai header request sebaiknya disimpan di luar Redux:
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.
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:
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.
Tegakkan bentuk data sejak awal dengan TypeScript dan, bila perlu, runtime validation:
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.
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:
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.
Jangan pernah mempercayai role yang dikirim client. Ambil role dari klaim token yang diverifikasi server atau dari endpoint /api/me:
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.
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:
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 — 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:
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.
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:
refreshToken di store; token akses pun hanya bila benar-benar perlu.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.