Episode ini membawa Redux ke server rendering: membuat store per-request di Next.js App Router, membungkus aplikasi dengan Provider, menghidrasi state dari server, serta mem-prefetch data RTK Query dengan dehydrate dan hydrate untuk Server Components.

Di aplikasi browser murni, store Redux cukup dibuat sekali sebagai singleton. Di Next.js dengan App Router, aturan itu berubah: halaman bisa di-render di server dan client secara terpisah, dan store global yang sama akan dibagikan antar request — memicu kebocoran state antar pengguna. Episode 14 membahas pola yang benar untuk integrasi Redux dengan Next.js.
Kita akan belajar membuat store per-request, menyuntikkan Provider di layout, menghidrasi state dari server, lalu menggabungkan RTK Query dengan Server Components lewat dehydrate dan hydrate. Hasilnya: halaman cepat karena data sudah tersedia di HTML awal, namun tetap interaktif karena state Redux tersinkron penuh.
Dalam satu proses server, Next.js menangani banyak request bersamaan. Store singleton menyimpan state pengguna A yang bisa terbaca oleh pengguna B. Solusinya: buat store baru untuk setiap request, dan pastikan kode membuat store hanya di sisi server — tidak pernah dieksekusi dua kali dalam satu request yang sama.
import { configureStore } from "@reduxjs/toolkit"
import { authSlice } from "../features/auth/authSlice"
import { postsSlice } from "../features/posts/postsSlice"
export const makeStore = () =>
configureStore({
reducer: {
auth: authSlice.reducer,
posts: postsSlice.reducer,
},
})
export type AppStore = ReturnType<typeof makeStore>
export type RootState = ReturnType<AppStore["getState"]>
export type AppDispatch = AppStore["dispatch"]makeStore adalah factory function. Setiap request memanggilnya sekali untuk mendapatkan store sendiri, sehingga tidak ada data yang bocor antar pengguna.
React bisa mengeksekusi komponen lebih dari sekali selama rendering. Karena itu simpan store dalam useRef agar hanya dibuat pada mount pertama:
import { useRef } from "react"
import { Provider } from "react-redux"
import { makeStore, type AppStore } from "./store"
export default function StoreProvider({ children }: { children: React.ReactNode }) {
const storeRef = useRef<AppStore>(null)
if (!storeRef.current) {
storeRef.current = makeStore()
}
return <Provider store={storeRef.current}>{children}</Provider>
}useRef memastikan makeStore hanya dijalankan sekali per komponen. Seluruh pohon React di bawahnya mendapat akses ke store yang sama.
Pasang StoreProvider di app/layout.tsx. Karena layout dirender di server maupun client, store per-request akan tersedia di seluruh halaman:
import StoreProvider from "../lib/store-hooks"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="id">
<body>
<StoreProvider>{children}</StoreProvider>
</body>
</html>
)
}Server merender HTML dengan state awal store, lalu client mengambil alih. Agar tidak terjadi mismatch hydration, state yang dipakai saat server render harus identik dengan yang dipakai saat client render pertama.
Perbedaan tanggal, random, atau data dari API yang di-fetch dua kali akan memicu error hydration. Trik paling sederhana: render konten yang bergantung browser hanya setelah mount dengan useEffect:
import { useEffect, useState } from "react"
export function ClientOnly({ children }) {
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
return children
}State yang benar-benar harus konsisten sejak awal sebaiknya dihidrasi dari server — tema berikutnya.
Server Components tidak bisa memakai hooks, termasuk hook query RTK. Pola resmi adalah membuat store sementara di server, me-dispatch endpoint untuk prefetch, lalu menyalin state ke client:
import { makeStore } from "../lib/store"
import { postsApi } from "../features/posts/postsApi"
import PostsViewer from "./PostsViewer"
export default async function PostsPage() {
const store = makeStore()
await store.dispatch(postsApi.endpoints.getPosts.initiate())
return (
<>
<PostsViewer />
</>
)
}initiate adalah action thunk yang dimiliki setiap endpoint. Di server, await store.dispatch(...) memastikan data sudah masuk cache sebelum HTML dikirim.
Cache RTK Query yang sudah terisi di server harus dibawa ke client. Gunakan createApi dengan serializeQueryArgs default lalu ambil cache-state dan deklarasi ulang lewat API client:
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
export const postsApi = createApi({
reducerPath: "postsApi",
baseQuery: fetchBaseQuery({ baseUrl: "https://jsonplaceholder.typicode.com" }),
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => "posts",
}),
}),
})Agar cache server bisa di-reuse, buka komponen client yang merender hasil dengan memakai useAppSelector dari store yang sudah dihidrasi:
"use client"
import { useGetPostsQuery } from "../features/posts/postsApi"
export default function PostsViewer() {
const { data: posts, isLoading } = useGetPostsQuery()
if (isLoading) return <p>Memuat...</p>
return <ul>{posts?.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
}RTK Query mendeteksi bahwa cache sudah terisi dan langsung memakai datanya tanpa request kedua — memanfaatkan siklus hidup cache yang kita bahas di episode 8.
Tip
Untuk aplikasi Next.js dengan App Router, dehydrate dan hydrate dari @reduxjs/toolkit/query/react bisa menyimpan seluruh cache-state dalam satu serializable objek yang direndam ke window.__PRELOADED_STATE__, lalu di-hydrate di client. Pilih satu pola yang konsisten: deklarasi store per-request, bukan keduanya.
Integrasi Redux dengan Next.js App Router menuntut disiplin: store harus dibuat per-request dengan factory function, Provider dipasang di layout root, state dihidrasi dari server agar tidak mismatch, dan RTK Query di-prefetch lewat initiate di server lalu dibaca kembali di client dari cache yang sama. Pola ini menghasilkan halaman cepat tanpa kehilangan keunggulan Redux.
Inti yang harus dibawa pulang:
makeStore agar tidak ada singleton antar request.useRef supaya tidak dibuat berkali-kali oleh React.Provider di app/layout.tsx agar tersedia di seluruh halaman.endpoints.x.initiate() pada store sementara.Episode 15 berikutnya membahas keamanan dan best practice state — mana data yang boleh masuk store, cara menangani token dan data sensitif, sanitasi data API, proteksi akses berbasis role, serta prinsip state minimal untuk aplikasi produksi.