Belajar Redux - Structure Store & combineSlices
Episode 11 of 23

Belajar Redux - Structure Store & combineSlices

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.

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

Pendahuluan

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.

combineSlices untuk Store Modular

Reducer per Fitur

Pola feature-based folders membagi kode per fitur: setiap fitur memiliki slice, selectors, dan komponennya sendiri. Store hanya menggabungkan slice yang sudah dideklarasikan:

JSsrc/features/auth/authSlice.ts
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 authSlice

Perhatikan 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.

Membuat Store dengan combineSlices

Kumpulkan seluruh slice di satu tempat. combineSlices menghubungkan reducer dan menyediakan fungsi injector untuk redux reducer tambahan:

JSsrc/app/store.ts
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.

Lazy Loading Reducer

Mengapa Perlu Code-Splitting

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:

JSInject reducer saat fitur 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:

JSTipe lazy slices di store.ts
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.

Merancang State Shape

Prinsip Satu Fitur Satu Slice

Bentuk state yang sehat mengikuti aturan sederhana:

  • Setiap domain data memiliki slice sendiri dengan nama deskriptif.
  • Slice tidak saling membaca field internal satu sama lain langsung di reducer.
  • Interaksi antar fitur dilakukan lewat action yang didispatch atau thunk yang memakai getState.

Contoh relasi antar entitas: daftar post menyimpan authorId, bukan salinan objek author. Komponen yang menampilkan nama author menggabungkan selectPostById dan selectUserById lewat createSelector:

JSKolaborasi antar slice via 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.

Menghindari Nested State yang Dalam

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:

State datar lebih mudah dirawat
{
  "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.

Penutup

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.
  • Satu domain data idealnya satu slice, tidak menulis ke slice lain.
  • Simpan relasi antar entitas sebagai id, gabungkan di selector dengan createSelector.
  • Jaga state sedatar mungkin; hindari nesting tiga level ke atas.

Episode 12 berikutnya membahas middleware — Redux Thunk, kapan menulis thunk manual versus memakai createAsyncThunk, serta createListenerMiddleware untuk merespons action secara terpusat tanpa mencemari reducer.

Belajar Redux - Structure Store & combineSlices | Belajar Redux