Skip to content

Entities ​

For CRUD-heavy applications with normalized collections, ngStato provides two utilities:

  • createEntityAdapter() — low-level adapter with CRUD operations and selectors
  • withEntities() — config wrapper that auto-generates state, actions, and selectors

createEntityAdapter() ​

Create an adapter ​

ts
import { createEntityAdapter } from '@ngstato/core'

type Product = { id: number; name: string; price: number; category: string }

const productAdapter = createEntityAdapter<Product>({
  selectId: (p) => p.id,                                // default: entity.id
  sortComparer: (a, b) => a.name.localeCompare(b.name)  // optional
})

Initial state ​

ts
const initialState = productAdapter.getInitialState()
// { ids: [], entities: {} }

// With extra state
const stateWithExtras = {
  ...productAdapter.getInitialState(),
  loading: false,
  error: null as string | null,
  selectedId: null as number | null
}

CRUD operations ​

All operations mutate the EntityState object directly:

ts
const state = productAdapter.getInitialState()

// Add
productAdapter.addOne(state, { id: 1, name: 'Widget', price: 9.99, category: 'tools' })
productAdapter.addMany(state, [product2, product3])

// Set (replace)
productAdapter.setAll(state, [product1, product2, product3])
productAdapter.setOne(state, product1)  // add or replace

// Update
productAdapter.updateOne(state, { id: 1, changes: { price: 12.99 } })
productAdapter.updateMany(state, [
  { id: 1, changes: { price: 10 } },
  { id: 2, changes: { price: 20 } }
])

// Remove
productAdapter.removeOne(state, 1)
productAdapter.removeMany(state, [1, 2, 3])
productAdapter.removeAll(state)

Selectors ​

ts
productAdapter.selectAll(state)       // Product[] (sorted if comparer set)
productAdapter.selectById(state, 1)   // Product | undefined
productAdapter.selectIds(state)       // number[]
productAdapter.selectTotal(state)     // number

Full store example ​

ts
import { createStore, createEntityAdapter, http } from '@ngstato/core'

type Product = { id: number; name: string; price: number }

const adapter = createEntityAdapter<Product>({
  selectId: (p) => p.id,
  sortComparer: (a, b) => a.name.localeCompare(b.name)
})

export const productStore = createStore({
  ...adapter.getInitialState(),
  loading: false,
  error: null as string | null,

  selectors: {
    allProducts: (state) => adapter.selectAll(state),
    productCount: (state) => adapter.selectTotal(state),
    productById: (state) => (id: number) => adapter.selectById(state, id)
  },

  actions: {
    async loadProducts(state) {
      state.loading = true
      try {
        const products = await http.get<Product[]>('/products')
        adapter.setAll(state, products)
      } catch (e) {
        state.error = (e as Error).message
      } finally {
        state.loading = false
      }
    },

    async createProduct(state, data: Omit<Product, 'id'>) {
      const product = await http.post<Product>('/products', data)
      adapter.addOne(state, product)
    },

    async updateProduct(state, id: number, changes: Partial<Product>) {
      const updated = await http.patch<Product>(`/products/${id}`, changes)
      adapter.updateOne(state, { id, changes: updated })
    },

    async deleteProduct(state, id: number) {
      await http.delete(`/products/${id}`)
      adapter.removeOne(state, id)
    }
  }
})

withEntities() ​

A config wrapper that auto-generates the entity slice, CRUD actions, and selectors:

ts
import { createStore, createEntityAdapter, withEntities } from '@ngstato/core'

type Student = { id: number; name: string; level: string }

const adapter = createEntityAdapter<Student>({ selectId: (s) => s.id })

export const studentsStore = createStore(
  withEntities(
    {
      loading: false,
      error: null as string | null,

      actions: {
        async loadAll(state) {
          state.loading = true
          state.error = null
          try {
            const data = await http.get<Student[]>('/students')
            // Use generated action
            adapter.setAll(state, data)
          } catch (e) {
            state.error = (e as Error).message
          } finally {
            state.loading = false
          }
        }
      }
    },
    { key: 'students', adapter }
  )
)

Generated state ​

PropertyTypeDescription
store.studentsIdsEntityId[]Array of all IDs
store.studentsEntitiesRecord<EntityId, Student>ID → entity map

Generated actions ​

ActionDescription
store.addStudents(...entities)Add entities
store.setStudents(...entities)Replace all entities
store.updateStudents(...updates)Partial update entities
store.removeStudents(...ids)Remove by IDs

Generated selectors ​

SelectorDescription
store.allStudentsArray of all entities
store.totalStudentsTotal count

When to use entities ​

ScenarioApproach
Simple list (< 50 items)Plain array in state
Large list with lookups by IDcreateEntityAdapter()
Full CRUD with generated APIwithEntities()
Multiple entity types per storeMultiple adapters

Next steps ​

Released under the MIT License.