Belajar Redux - createEntityAdapter & Normalization
Episode 10 of 23

Belajar Redux - createEntityAdapter & Normalization

Episode ini membahas normalisasi state: createEntityAdapter untuk mengelola koleksi dengan CRUD terotomatisasi seperti setAll, upsertOne, dan removeMany, plus selector bawaan selectAll, selectById, dan pengurutan data yang konsisten.

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

Pendahuluan

Menampung data koleksi di dalam array saja terasa mudah di awal, tetapi menjadi rapuh saat aplikasi tumbuh. Update satu item berarti mencari posisinya di array, menghapus satu item berarti filter, dan dua komponen yang membutuhkan item yang sama menyimpan duplikat. Episode 10 memperkenalkan normalisasi state dan alat bantunya, createEntityAdapter, yang mengelola koleksi dengan pola ids + entities.

Pola ids + entities menyimpan setiap item sekali dalam kamus yang dinomori id, dan urutan disimpan terpisah sebagai daftar id. Hasilnya: lookup oleh id berjalan O(1), tidak ada duplikasi data antar slice, dan seluruh operasi CRUD tersedia sebagai helper yang sudah teruji.

Memahami Normalisasi ids dan entities

Bentuk State yang Ter-normalisasi

Perhatikan bentuk state yang diproduksi adapter:

State ter-normalisasi
{
  "ids": [1, 2, 3],
  "entities": {
    "1": { "id": 1, "title": "Halo Dunia" },
    "2": { "id": 2, "title": "Redux Toolkit" },
    "3": { "id": 3, "title": "Normalisasi Data" }
  }
}

ids menyimpan urutan, entities menyimpan data. Perbarui item nomor 2 cukup dengan menimpa entri entities["2"] — tanpa menyentuh item lain. Relasi antar entitas juga jadi lebih mudah, misalnya menyimpan array userId di dalam objek post, bukan menyalin seluruh data user.

Mengapa Ini Penting

  • Mengurangi duplikasi data dan risiko ketidakkonsistenan antar slice.
  • Update dan delete menjadi O(1) karena berbasis id.
  • Cache selectors lebih efektif karena referensi objek stabil.

Membuat Adapter dan Slice

createEntityAdapter

createEntityAdapter menerima opsi selectId untuk koleksi tanpa field id, dan sortComparer untuk mengatur urutan default:

JSsrc/features/posts/postsAdapter.ts
import { createEntityAdapter } from "@reduxjs/toolkit"
 
interface Post {
  id: number
  title: string
  createdAt: number
}
 
export const postsAdapter = createEntityAdapter<Post>({
  selectId: (post) => post.id,
  sortComparer: (a, b) => a.createdAt - b.createdAt,
})

sortComparer menentukan urutan ids. Di sini post dengan createdAt lebih kecil diletakkan lebih dulu, sehingga daftar otomatis terurut berdasarkan waktu pembuatan. Jika dihilangkan, urutan mengikuti urutan penambahan.

Slice Berbasis Adapter

Adapter menyediakan reducer siap pakai: setAll, addOne, addMany, upsertOne, upsertMany, updateOne, removeOne, dan removeMany. Semua dipakai bersama Immer di dalam reducers:

JSsrc/features/posts/postsSlice.ts
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import { postsAdapter } from "./postsAdapter"
 
export const fetchPosts = createAsyncThunk(
  "posts/fetchPosts",
  async () => {
    const res = await fetch("/api/posts")
    return res.json()
  },
)
 
const postsSlice = createSlice({
  name: "posts",
  initialState: postsAdapter.getInitialState({ status: "idle" }),
  reducers: {
    addPost: postsAdapter.addOne,
    removePost: postsAdapter.removeOne,
    removeManyPosts: postsAdapter.removeMany,
  },
  extraReducers: (builder) => {
    builder.addCase(fetchPosts.fulfilled, (state, action) => {
      postsAdapter.setAll(state, action.payload)
      state.status = "succeeded"
    })
  },
})

getInitialState membuat state awal berisi ids kosong, entities kosong, plus field tambahan status. Satu baris postsAdapter.setAll(state, action.payload) menggantikan seluruh logika manual menyimpan hasil fetch.

CRUD Terotomatisasi dalam Praktik

Menambah dan Mengupdate

Tiga operasi yang paling sering dipakai: upsertOne untuk menyisipkan item baru atau menimpa yang lama, addMany untuk menambahkan banyak item sekaligus, dan updateOne untuk memodifikasi item berdasarkan id:

JSReducers CRUD lengkap
import { createSlice } from "@reduxjs/toolkit"
import { postsAdapter } from "./postsAdapter"
 
const postsSlice = createSlice({
  name: "posts",
  initialState: postsAdapter.getInitialState({ status: "idle" }),
  reducers: {
    upsertPost: postsAdapter.upsertOne,
    updatePost: {
      reducer: (state, action) => {
        postsAdapter.updateOne(state, action.payload)
      },
      prepare: (id: number, changes: Partial<Post>) => ({
        payload: { id, changes },
      }),
    },
    clearPosts: postsAdapter.removeAll,
  },
})

updateOne menerima objek { id, changes } sehingga reducer tetap satu baris. Seluruh helper menerima state sebagai argumen pertama dan mengembalikan hasil baru — tidak perlu return manual.

Menghapus Banyak Item

Untuk operasi massal seperti memfilter list, removeMany menerima array id atau predikat fungsi:

JSMenghapus berdasarkan predikat
removeOldPosts: (state, action) => {
  const cutoff = action.payload
  postsAdapter.removeMany(state, (post) => post.createdAt < cutoff)
}

Predikat dikirim ke removeMany sebagai callback yang mengevaluasi setiap entitas. Adapter menghapus semua item yang memenuhi kondisi dalam satu operasi state.

Selector Bawaan dan Sorting

selectAll, selectById, dan selectEntities

Adapter menyediakan selector yang bekerja terhadap state slice. Agar type-safe, gabungkan dengan selector tingkat root:

JSsrc/features/posts/postsSelectors.ts
import { createSelector } from "@reduxjs/toolkit"
import { postsAdapter } from "./postsAdapter"
import type { RootState } from "../../app/store"
 
export const { selectAll, selectById, selectEntities, selectIds } =
  postsAdapter.getSelectors((state: RootState) => state.posts)
 
export const selectPostById = (id: number) =>
  createSelector(selectById, (entity) => entity?.[id])
 
export const selectRecentPosts = createSelector(
  [selectAll],
  (posts) => posts.filter((p) => p.createdAt > Date.now() - 86400000).slice(0, 5),
)

getSelectors menerima fungsi pemilih state slice, lalu menghasilkan selector yang langsung membaca state root. selectById pada dasarnya memilih seluruh entities; bungkus dengan createSelector bila ingin mengambil satu item secara spesifik.

Tip

selectAll menghasilkan array baru setiap kali dipanggil jika kalian memfilter di komponen. Lakukan komposisi lewat createSelector seperti contoh di atas supaya hasilnya tetap dimemoize dan tidak memicu re-render berlebihan.

Penutup

Normalisasi dengan createEntityAdapter mengubah cara kita mengelola koleksi: state yang sebelumnya array penuh kalkulasi manual kini menjadi pasangan ids + entities dengan CRUD satu baris. Operasi berbasis id jadi murah, urutan diatur konsisten lewat sortComparer, dan selector bawaan membuat akses data di komponen tetap ringkas.

Inti yang harus dibawa pulang:

  • State normal terdiri dari ids untuk urutan dan entities untuk kamus data.
  • createEntityAdapter menghasilkan reducer CRUD: setAll, upsertOne, updateOne, removeMany.
  • getInitialState dapat diberi field tambahan seperti status.
  • getSelectors menghasilkan selectAll, selectById, selectEntities, dan selectIds.
  • sortComparer menjaga urutan ids tetap konsisten.
  • Kombinasikan adapter dengan createSelector untuk derived data yang dimemoize.

Episode 11 berikutnya membahas struktur store & combineSlices — bagaimana menggabungkan reducer modular, memanfaatkan lazy loading untuk code-splitting, dan merancang bentuk state per fitur yang sehat untuk aplikasi besar.

Belajar Redux - createEntityAdapter & Normalization | Belajar Redux