API Reference
createStore(options)
Creates a new Inglorious Store instance. Returns a Redux-compatible store.
const store = createStore({
types, // Object: entity type definitions (required)
entities, // Object: initial entities (required)
systems, // Array: global state handlers (optional)
autoCreateEntities, // Boolean: auto-create singleton entities (optional)
updateMode, // String: 'auto' | 'manual' (optional)
})
2
3
4
5
6
7
Options
types (required)
Object defining entity type behaviors. Each type is a behavior object or array of behaviors:
const types = {
todoList: {
addTodo(entity, text) {
entity.todos.push({ id: Date.now(), text, done: false })
},
removeTodo(entity, id) {
entity.todos = entity.todos.filter((t) => t.id !== id)
},
},
counter: {
increment(entity) {
entity.value++
},
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
entities (required)
Object containing initial entity instances:
const entities = {
workTodos: { type: "TodoList", todos: [] },
personalTodos: { type: "TodoList", todos: [] },
counter: { type: "Counter", value: 0 },
}
2
3
4
5
systems (optional)
Array of global state handlers that run after all entity handlers:
const systems = [
{
taskCompleted(state, prevState) {
// Calculate derived state, validate consistency, etc.
console.log("All handlers finished, state updated")
},
},
]
const store = createStore({ types, entities, systems })
2
3
4
5
6
7
8
9
10
autoCreateEntities (optional)
Boolean flag to automatically create singleton entities for types not defined in entities:
const store = createStore({
types: {
settings: {
setTheme(e, t) {
e.theme = t
},
},
analytics: {
track(e, ev) {
e.events.push(ev)
},
},
},
entities: {},
autoCreateEntities: true,
})
// Automatically creates:
// settings: { type: "Settings" }
// analytics: { type: "Analytics" }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Use when:
- ✅ Building web apps with singleton services
- ❌ Managing multiple instances (game development)
- ❌ When you need fine-grained initial state
updateMode (optional)
Controls when the store notifies subscribers:
'auto'(default) - Subscribers notified after each event'manual'- Callapi.update()to batch updates
const store = createStore({
types,
entities,
updateMode: "manual",
})
// Multiple events, single update
store.notify("playerMoved", { x: 100, y: 50 })
store.notify("enemyAttacked", { damage: 10 })
store.update() // Subscribers notified once
2
3
4
5
6
7
8
9
10
Store Methods
notify(eventType, payload)
Trigger an event. All entities with a handler for that event will process it.
// Broadcast to all entities
store.notify("taskCompleted", "task123")
// Targeted notifications
store.notify("#entityId:eventName", payload) // Specific entity
store.notify("typeName:eventName", payload) // Specific type
store.notify("type#id:eventName", payload) // Type + entity
2
3
4
5
6
7
dispatch(action)
Redux-compatible dispatch. Use notify() instead (cleaner):
store.dispatch({ type: "eventName", payload })
// Same as: store.notify("eventName", payload)
2
subscribe(listener)
Redux-compatible subscription:
const unsubscribe = store.subscribe(() => {
console.log("State changed!", store.getState())
})
// Later:
unsubscribe()
2
3
4
5
6
getState()
Get the current state object:
const state = store.getState()
console.log(state.workTodos.todos)
2
update()
For manual update mode only. Process queued events and notify subscribers:
const store = createStore({ types, entities, updateMode: "manual" })
store.notify("event1", payload1)
store.notify("event2", payload2)
store.update() // Subscribers notified once
2
3
4
5
replaceReducer(nextReducer)
Redux-compatible method. Not typically used with Inglorious Store. For advanced use cases only.
Event Handler API
Event handlers receive three arguments:
const types = {
todoList: {
// Handler with all three arguments
addTodo(entity, payload, api) {
// entity: the entity being updated (mutate freely)
// payload: data passed to notify()
// api: store methods
},
// Handlers can omit unused arguments
refresh(entity) {
entity.lastRefresh = Date.now()
},
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Handler Arguments
entity
The entity instance. Mutate freely — immutability is guaranteed:
toggle(entity, id) {
const item = entity.items.find((i) => i.id === id)
if (item) item.done = !item.done // Looks like mutation, is immutable
}
2
3
4
payload
The data passed with the event:
store.notify("addTodo", "Buy milk")
// Handler receives "Buy milk" as payload
addTodo(entity, payload) {
entity.todos.push({ text: payload, done: false })
}
2
3
4
5
6
api
Access to store operations:
async fetchData(entity, userId, api) {
entity.loading = true
const data = await fetch(`/api/${userId}`).then((r) => r.json())
// Trigger another event
api.notify("dataLoaded", data)
// Read other entities
const user = api.getEntity("user")
const allEntities = api.getEntities()
// Access type definitions (advanced)
const typeDef = api.getType("todoList")
const allTypes = api.getTypes()
// Change type behavior at runtime (advanced)
api.setType("counter", { /* new behavior */ })
// Manual batch control (if updateMode: "manual")
api.update()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
API Methods
api.getEntities(typeName?)
Returns entities from current state (read-only):
// Full entities map (keyed by id)
const allEntities = api.getEntities()
const workTodos = allEntities.workTodos
// Filter by entity type (array)
const todos = api.getEntities("todo")
2
3
4
5
6
api.getEntity(id)
Get a specific entity (read-only):
const user = api.getEntity("user")
const userId = user.id
2
api.select(selector)
Run a selector against the current state:
const activeFilter = (state) => state.toolbar.activeFilter
const filter = api.select(activeFilter)
2
api.notify(eventType, payload)
Trigger another event (queued with current event):
async login(entity, credentials, api) {
const user = await authenticateUser(credentials)
api.notify("loginSuccess", user)
api.notify("analytics:logLogin", user)
}
loginSuccess(entity, user) {
entity.user = user
entity.loggedIn = true
}
2
3
4
5
6
7
8
9
10
api.dispatch(action)
Redux-style dispatch (less common, use notify instead):
api.dispatch({ type: "customEvent", payload })
api.getTypes()
Get all type definitions (read-only):
const types = api.getTypes()
const todoListType = types.todoList
2
api.getType(typeName)
Get a specific type definition:
const type = api.getType("todoList")
api.setType(typeName, newType)
Change a type's behavior at runtime (advanced):
store.notify("upgradeType", {
typeName: "counter",
behavior: { increment: () => {}, doubleIncrement: () => {} }
})
upgradeType(entity, { typeName, behavior }, api) {
api.setType(typeName, behavior)
}
2
3
4
5
6
7
8
api.update()
For manual update mode: process events and notify subscribers:
// This only exists/works if updateMode: "manual"
api.update()
2
Built-in Events
create
Triggered when an entity is added via the add event. Visible only to the created entity (not broadcast):
const types = {
todoList: {
create(entity) {
// Initialize new list
entity.createdAt = Date.now()
entity.todos = entity.todos || []
},
},
}
store.notify("add", { id: "work", type: "TodoList", todos: [] })
// create handler fires only for "work" entity
2
3
4
5
6
7
8
9
10
11
12
destroy
Triggered when an entity is removed via the remove event. Visible only to the destroyed entity (not broadcast):
const types = {
file: {
destroy(entity, payload, api) {
// Cleanup
console.log(`Destroying file: ${entity.id}`)
// Save to database, close connections, etc.
},
},
}
store.notify("remove", { id: "tempFile" })
// destroy handler fires only for "tempFile" entity
2
3
4
5
6
7
8
9
10
11
12
add
Built-in event to add a new entity:
store.notify("add", {
id: "newList",
type: "TodoList",
todos: [],
priority: "high",
})
// Triggers: create event for newList
// Result: newList entity appears in state
2
3
4
5
6
7
8
9
remove
Built-in event to remove an entity:
store.notify("remove", { id: "workTodos" })
// Triggers: destroy event for workTodos
// Result: workTodos entity removed from state
2
3
4
handleAsync(eventName, handlers, options?)
Helper to generate lifecycle events for async operations:
import { handleAsync } from "@inglorious/store/async"
const types = {
todoList: {
...handleAsync("fetchTodos", {
async run(entity, userId, api) {
const todos = await fetch(`/api/users/${userId}/todos`).then((r) =>
r.json(),
)
api.notify("success", todos)
},
start(entity) {
entity.loading = true
},
success(entity, todos) {
entity.todos = todos
entity.loading = false
},
error(entity, error) {
entity.error = error
entity.loading = false
},
}),
},
}
store.notify("fetchTodos", userId)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Think of the flow like a newspaper article:
startwrites the headline and sets the scenerungathers the reportingsuccesspublishes the storyerrorprints the correctionfinallyarchives the notes
Generated events:
fetchTodos- Trigger the async operationfetchTodosRun- The async function runsfetchTodosSuccess- Success handler firesfetchTodosError- Error handler firesfetchTodosFinally- Always fires (optional)
Optimistic Updates
For optimistic UI state, wrap the generated behavior with optimistic. This helper lives at @inglorious/store/optimistic, so only users who need it import it:
import { handleAsync } from "@inglorious/store/async"
import { optimistic } from "@inglorious/store/optimistic"
const todoList = {
...optimistic(
handleAsync("saveTodo", {
async run(payload) {
const res = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(payload),
})
if (!res.ok) throw new Error("Failed")
return res.json()
},
success(entity, todo) {
entity.todos = entity.todos.map((item) =>
item.id === todo.tempId ? todo : item,
)
},
}),
(entity, payload) => ({
todos: [
...entity.todos,
{
id: payload.tempId,
title: payload.title,
completed: payload.completed,
pending: true,
},
],
}),
),
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Options
handleAsync(
"fetchTodos",
{
/* handlers */
},
{
scope: "entity", // "entity" | "type" | "global"
// "entity": #entityId:fetchTodosSuccess
// "type": todoList:fetchTodosSuccess
// "global": fetchTodosSuccess
},
)
2
3
4
5
6
7
8
9
10
11
12
compute(fn, selectors)
Create memoized derived state:
import { compute } from "@inglorious/store"
const selectTotal = compute(
(work, personal) => work.length + personal.length,
[(state) => state.workTodos.todos, (state) => state.personalTodos.todos],
)
// Works with selectors
const total = useSelector(selectTotal)
2
3
4
5
6
7
8
9
createSelector(inputSelectors, resultFn)
Redux-compatible selector (alias for compute):
import { createSelector } from "@inglorious/store"
const selectTotalTodos = createSelector(
[(state) => state.workTodos.todos, (state) => state.personalTodos.todos],
(work, personal) => work.length + personal.length,
)
const total = useSelector(selectTotalTodos)
2
3
4
5
6
7
8
Notify vs Dispatch
Both work, but notify() is the preferred API:
// Preferred: cleaner and more explicit
store.notify("addTodo", text)
// Still works: Redux-compatible
store.dispatch({ type: "addTodo", payload: text })
2
3
4
5
React Integration
Inglorious Store works with react-redux:
import { Provider, useSelector, useDispatch } from "react-redux"
import { createStore } from "@inglorious/store"
const store = createStore({ types, entities })
function App() {
return (
<Provider store={store}>
<TodoList />
</Provider>
)
}
function TodoList() {
const todos = useSelector((state) => state.workTodos.todos)
const dispatch = useDispatch()
return (
<button onClick={() => dispatch({ type: "addTodo", payload: "..." })}>
Add
</button>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
For a better React experience, use @inglorious/react-store:
import { createReactStore } from "@inglorious/react-store"
const { Provider, useSelector, useNotify } = createReactStore(store)
function TodoList() {
const todos = useSelector((state) => state.workTodos.todos)
const notify = useNotify()
return <button onClick={() => notify("addTodo", "...")}>Add</button>
}
2
3
4
5
6
7
8
9
10
Type Safety (TypeScript)
Define types for your store:
import type { TypesConfig } from "@inglorious/store"
interface TodoListEntity {
type: "TodoList"
todos: Array<{ id: number; text: string; done: boolean }>
}
interface TodoListState {
workTodos: TodoListEntity
personalTodos: TodoListEntity
}
interface TodoListTypes extends TypesConfig<TodoListEntity> {
todoList: {
addTodo(entity: TodoListEntity, text: string): void
toggle(entity: TodoListEntity, id: number): void
}
}
const types: TodoListTypes = {
todoList: {
addTodo(entity, text) {
entity.todos.push({ id: Date.now(), text, done: false })
},
toggle(entity, id) {
const todo = entity.todos.find((t) => t.id === id)
if (todo) todo.done = !todo.done
},
},
}
const store = createStore<TodoListEntity, TodoListState>({
types: types as unknown as TypesConfig<TodoListEntity>,
entities,
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
RTK Migration Utilities
Inglorious Store provides utilities to help migrate from Redux Toolkit:
convertAsyncThunk(eventName, asyncFn, options?)
import { convertAsyncThunk } from "@inglorious/store/rtk"
const handlers = convertAsyncThunk("fetchTodos", fetchTodosAsync, {
onPending: (entity) => (entity.loading = true),
onError: (entity, error) => (entity.error = error),
onSuccess: (entity, data) => (entity.todos = data),
})
const types = {
todoList: { ...handlers },
}
2
3
4
5
6
7
8
9
10
11
convertSlice(slice, options?)
import { convertSlice } from "@inglorious/store/rtk"
const todoListType = convertSlice(todosSlice, {
asyncThunks: {
fetchTodos: fetchTodosAsync,
},
})
const types = {
todoList: todoListType,
}
2
3
4
5
6
7
8
9
10
11
createRTKCompatDispatch(api, entityId)
import { createRTKCompatDispatch } from "@inglorious/store/rtk"
const dispatch = createRTKCompatDispatch(api, "todos")
dispatch({ type: "todos/addTodo", payload: "Buy milk" })
2
3
4
For more details, see Migrating from Redux & RTK.
Inglorious Store