Episode ini membahas arsitektur store untuk aplikasi besar: combineSlices untuk menggabungkan reducer modular, lazy loading reducer lewat store.inject, pattern rootReducer, serta merancang bentuk state per fitur dan kolaborasi antar slice yang sehat.

Saat aplikasi hanya punya satu atau dua fitur, menaruh semua reducer di satu file terasa wajar. Tetapi begitu fitur bertambah — auth, cart, notifications, users — satu store raksasa menjadi sulit dirawat. Episode 11 membahas struktur store yang benar: memecah reducer per fitur, menggabungkannya secara modular, dan memungkinkan fitur dimuat belakangan lewat lazy loading.
Redux Toolkit menyediakan combineSlices sebagai pengganti modern dari combineReducers klasik. Ia memungkinkan reducer di-inject ke store secara runtime — dasar dari code-splitting redux di aplikasi framework. Selain itu kita bahas bagaimana merancang bentuk state agar setiap fitur mandiri namun bisa berkolaborasi.
Pola feature-based folders membagi kode per fitur: setiap fitur memiliki slice, selectors, dan komponennya sendiri. Store hanya menggabungkan slice yang sudah dideklarasikan:
import { createSlice } from "@reduxjs/toolkit"
interface AuthState {
user: string | null
token: string | null
}
const authSlice = createSlice({
name: "auth",
initialState: { user: null, token: null } satisfies AuthState,
reducers: {
loggedIn: (state, action) => {
state.user = action.payload.user
state.token = action.payload.token
},
loggedOut: (state) => {
state.user = null
state.token = null
},
},
})
export const { loggedIn, loggedOut } = authSlice.actions
export default authSlicePerhatikan bahwa reducer ditulis sebagai objek slice, bukan reducer function. combineSlices membutuhkan objek slice agar tahu kunci state-nya: nama slice auth otomatis menjadi kunci di store.
Kumpulkan seluruh slice di satu tempat. combineSlices menghubungkan reducer dan menyediakan fungsi injector untuk redux reducer tambahan:
import { combineSlices, configureStore } from "@reduxjs/toolkit"
import { authSlice } from "../features/auth/authSlice"
import { postsSlice } from "../features/posts/postsSlice"
export const rootReducer = combineSlices(authSlice, postsSlice)
export type RootState = ReturnType<typeof rootReducer>
export const store = configureStore({
reducer: rootReducer,
})Sekarang state berbentuk { auth: ..., posts: ... }. RootState diturunkan langsung dari rootReducer, sehingga setiap kali slice ditambahkan ke combineSlices, tipe state ikut diperbarui otomatis.
Fitur admin yang jarang dibuka tidak perlu dimuat bersamaan dengan halaman utama. Dengan store.inject, reducer fitur baru hanya masuk ke store ketika fitur itu benar-benar dimuat:
import { createSlice } from "@reduxjs/toolkit"
import { store } from "../../app/store"
export const adminSlice = createSlice({
name: "admin",
initialState: { logs: [] as string[] },
reducers: {
logAdded: (state, action) => {
state.logs.push(action.payload)
},
},
})
store.inject(adminSlice)store.inject(slice) menjalankan tiga hal sekaligus: menambahkan reducer ke kombinasi, memperbarui RootState (dalam runtime), dan menandai reducer sebagai sudah terdaftar sehingga panggilan kedua tidak membuat duplikat. CombineSlices juga mendukung opsi withLazyLoadedSlices untuk mendaftarkan tipe reducer yang belum dimuat di awal:
import { combineSlices } from "@reduxjs/toolkit"
const rootReducer = combineSlices(authSlice, postsSlice)
.withLazyLoadedSlices<{ admin: typeof adminSlice }>()Dengan deklarasi di atas, TypeScript memahami bahwa kunci admin belum ada namun akan di-inject kemudian — inject yang terlewat akan menghasilkan error saat compile, bukan diam-diam rusak di runtime.
Bentuk state yang sehat mengikuti aturan sederhana:
getState.Contoh relasi antar entitas: daftar post menyimpan authorId, bukan salinan objek author. Komponen yang menampilkan nama author menggabungkan selectPostById dan selectUserById lewat createSelector:
import { createSelector } from "@reduxjs/toolkit"
import { selectPostById } from "../posts/postsSelectors"
import { selectUserById } from "../users/usersSelectors"
export const selectPostWithAuthor = (postId: number) =>
createSelector(
[selectPostById(postId), selectUserById],
(post, users) => {
if (!post) return null
return { ...post, author: users[post.authorId] }
},
)Kedua selector membaca state dari slice yang berbeda, tetapi komposisi terjadi di selector level — bukan dengan memutasi slice lain dari dalam reducer. State tetap termodularisasi, sementara kolaborasi antar fitur tetap mungkin.
Tip
Jangan membuat slice menulis ke state slice lain. Jika dua fitur harus sinkron, pilih salah satu: action yang sama dihandle dua reducer (pattern action sharing), atau dispatch terkoordinasi di thunk.
Simpan state sedatar mungkin. Struktur berlapis tiga level seperti cart.items.meta.source menyulitkan update, normalisasi, dan debugging. Pecah menjadi slice terpisah dan hubungkan lewat id:
{
"cart": { "items": [{ "productId": 9, "qty": 2 }] },
"products": { "ids": [9], "entities": { "9": { "id": 9, "name": "Meja" } } }
}cart menyimpan productId dan qty saja; detail produk hidup di slice products. Update harga produk tidak menyentuh cart sama sekali — salah satu keuntungan normalisasi yang juga kita bahas di episode 10.
combineSlices mengubah cara store dibangun: reducer per fitur dikumpulkan deklaratif, lazy loading memungkinkan fitur berat dimuat hanya saat dibutuhkan, dan withLazyLoadedSlices menjaga type safety selama proses inject. Merancang state shape yang datar dan termodularisasi membuat store tetap mudah di-audit meski aplikasi terus membesar.
Inti yang harus dibawa pulang:
combineSlices menggabungkan slice dan menurunkan RootState secara otomatis.store.inject(slice) menambahkan reducer saat runtime untuk lazy loading.withLazyLoadedSlices mendeklarasikan tipe reducer yang di-inject belakangan.createSelector.Episode 12 berikutnya membahas middleware — Redux Thunk, kapan menulis thunk manual versus memakai createAsyncThunk, serta createListenerMiddleware untuk merespons action secara terpusat tanpa mencemari reducer.