insertEntities
The insertEntities insertion adds entity collection management methods to state, query, or queryParam primitives. It provides a type-safe way to manipulate arrays of entities with common operations like add, remove, update, and upsert.
Import
import { insertEntities } from '@craft-ng/core';
import {
addOne,
addMany,
removeOne,
removeMany,
setOne,
setMany,
setAll,
updateOne,
updateMany,
upsertOne,
upsertMany,
removeAll,
} from '@craft-ng/core';Overview
insertEntities bridges entity utility functions with reactive primitives by:
- Adding methods - Automatically generates typed methods from entity utilities
- Path support - Works with nested properties using dot notation
- Custom identifiers - Supports custom ID selectors beyond default
idproperty - Parallel queries - Enables entity manipulation in query instances with
selectparameter - Type inference - Full TypeScript support with automatic method name generation
Entity Utilities
The following entity utility functions can be used with insertEntities:
| Utility | Description |
|---|---|
addOne | Adds a single entity to the end |
addMany | Adds multiple entities to the end |
setOne | Replaces or adds an entity by ID |
setMany | Replaces or adds multiple entities by ID |
setAll | Replaces the entire collection |
updateOne | Partially updates an entity by ID |
updateMany | Partially updates multiple entities by ID |
upsertOne | Updates if exists, otherwise adds |
upsertMany | Updates multiple if exist, otherwise adds |
removeOne | Removes a single entity by ID |
removeMany | Removes multiple entities by ID |
removeAll | Clears the entire collection |
Signature
function insertEntities<State, K, EntityHelperFns, Path>(config: {
methods: EntityHelperFns;
identifier?: IdSelector<Entity, K>;
path?: Path; // For nested arrays in objects
}): Insertion;Parameters
methods
Array of entity utility functions to expose as methods on the state/query.
identifier (optional)
Custom function to extract the unique identifier from entities. Defaults to:
- For objects with
idproperty:(entity) => entity.id - For primitives (string/number):
(entity) => entity
path (optional)
Dot-notation path to a nested array property. When provided, method names are prefixed with the camelCase path.
Example: path: 'catalog.products' → methods like catalogProductsAddOne()
Method Naming
- Without path: Method names match utility function names (e.g.,
addOne,removeMany) - With path: Method names are prefixed with camelCase path (e.g.,
productsAddOne,catalogProductsRemoveMany)
Examples
Basic entity management with primitives
import {
state,
insertEntities,
addOne,
addMany,
removeOne,
} from '@craft-ng/core';
const tags = state(
[] as string[],
insertEntities({
methods: [addOne, addMany, removeOne],
}),
);
// Add single tag
tags.addOne({ entity: 'typescript' });
console.log(tags()); // ['typescript']
// Add multiple tags
tags.addMany({ newEntities: ['angular', 'signals'] });
console.log(tags()); // ['typescript', 'angular', 'signals']
// Remove tag
tags.removeOne({ id: 'typescript' });
console.log(tags()); // ['angular', 'signals']Managing objects with default ID
import {
state,
insertEntities,
addOne,
setOne,
removeOne,
} from '@craft-ng/core';
interface Product {
id: string;
name: string;
price: number;
}
const products = state(
[] as Product[],
insertEntities({
methods: [addOne, setOne, removeOne],
}),
);
// Add product
products.addOne({
entity: { id: '1', name: 'Laptop', price: 999 },
});
// Replace or update product
products.setOne({
entity: { id: '1', name: 'Laptop Pro', price: 1299 },
});
console.log(products()); // [{ id: '1', name: 'Laptop Pro', price: 1299 }]
// Remove product
products.removeOne({ id: '1' });
console.log(products()); // []Using custom identifier
import { state, insertEntities, setOne, removeOne } from '@craft-ng/core';
interface User {
uuid: string;
name: string;
email: string;
}
const users = state(
[] as User[],
insertEntities({
methods: [setOne, removeOne],
identifier: (user) => user.uuid,
}),
);
users.setOne({
entity: { uuid: 'abc-123', name: 'Alice', email: 'alice@example.com' },
});
users.setOne({
entity: { uuid: 'abc-123', name: 'Alice Smith', email: 'alice@example.com' },
});
console.log(users());
// [{ uuid: 'abc-123', name: 'Alice Smith', email: 'alice@example.com' }]
users.removeOne({ id: 'abc-123' });
console.log(users()); // []Working with nested arrays using path
import { state, insertEntities, addMany, removeOne } from '@craft-ng/core';
interface Catalog {
total: number;
products: Array<{ id: string; name: string }>;
}
const catalog = state(
{
total: 0,
products: [],
} as Catalog,
insertEntities({
methods: [addMany, removeOne],
path: 'products',
}),
);
// Methods are prefixed with "products"
catalog.productsAddMany({
newEntities: [
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' },
],
});
console.log(catalog());
// { total: 0, products: [{ id: '1', name: 'Item 1' }, { id: '2', name: 'Item 2' }] }
catalog.productsRemoveOne({ id: '1' });
console.log(catalog());
// { total: 0, products: [{ id: '2', name: 'Item 2' }] }Deep nested path with dot notation
import { state, insertEntities, addMany } from '@craft-ng/core';
interface State {
catalog: {
featured: {
products: Array<{ id: string; name: string }>;
};
};
}
const store = state(
{
catalog: {
featured: {
products: [],
},
},
} as State,
insertEntities({
methods: [addMany],
path: 'catalog.featured.products',
}),
);
// Method is prefixed with camelCase: catalogFeaturedProducts
store.catalogFeaturedProductsAddMany({
newEntities: [{ id: '1', name: 'Featured Item' }],
});
console.log(store().catalog.featured.products);
// [{ id: '1', name: 'Featured Item' }]Using with query primitive
import { query, insertEntities, addMany, removeOne } from '@craft-ng/core';
interface Product {
id: string;
name: string;
}
const productsQuery = query(
{
params: () => 'all',
loader: async () => {
const response = await fetch('/api/products');
return response.json() as Product[];
},
},
insertEntities({
methods: [addMany, removeOne],
}),
);
// After query loads, manipulate the cached data
await productsQuery.load();
// Add optimistic product
productsQuery.addMany({
newEntities: [{ id: 'temp-1', name: 'New Product' }],
});
// Remove product from cache
productsQuery.removeOne({ id: 'temp-1' });Working with parallel queries
import { query, insertEntities, addOne } from '@craft-ng/core';
const userQuery = query(
{
params: () => 'userId',
identifier: (params) => params, // Track multiple query instances
loader: async ({ params }) => {
const response = await fetch(`/api/users/${params}/posts`);
return response.json();
},
},
insertEntities({
methods: [addOne],
}),
);
// Manipulate specific query instance with select parameter
userQuery.addOne({
select: 'user-123', // Target specific query instance
entity: { id: 'post-1', title: 'New Post' },
});Update operations
import { state, insertEntities, updateOne, updateMany } from '@craft-ng/core';
interface Todo {
id: string;
title: string;
completed: boolean;
}
const todos = state(
[
{ id: '1', title: 'Learn Angular', completed: false },
{ id: '2', title: 'Build app', completed: false },
] as Todo[],
insertEntities({
methods: [updateOne, updateMany],
}),
);
// Update single todo
todos.updateOne({
update: {
id: '1',
changes: { completed: true },
},
});
console.log(todos()[0].completed); // true
// Update multiple todos
todos.updateMany({
updates: [
{ id: '1', changes: { title: 'Learn Angular Signals' } },
{ id: '2', changes: { completed: true } },
],
});Upsert operations
import { state, insertEntities, upsertOne, upsertMany } from '@craft-ng/core';
interface Settings {
key: string;
value: string;
}
const settings = state(
[{ key: 'theme', value: 'dark' }] as Settings[],
insertEntities({
methods: [upsertOne, upsertMany],
identifier: (setting) => setting.key,
}),
);
// Updates existing or adds new
settings.upsertOne({
entity: { key: 'theme', value: 'light' },
});
console.log(settings());
// [{ key: 'theme', value: 'light' }]
settings.upsertMany({
newEntities: [
{ key: 'theme', value: 'auto' },
{ key: 'language', value: 'en' },
],
});
console.log(settings());
// [
// { key: 'theme', value: 'auto' },
// { key: 'language', value: 'en' }
// ]Complete CRUD example
import {
state,
insertEntities,
addOne,
setOne,
updateOne,
removeOne,
setAll,
} from '@craft-ng/core';
interface Task {
id: string;
title: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
}
const tasks = state(
[] as Task[],
insertEntities({
methods: [addOne, setOne, updateOne, removeOne, setAll],
}),
);
// Create
tasks.addOne({
entity: {
id: '1',
title: 'Review code',
completed: false,
priority: 'high',
},
});
// Read - use tasks() to access the array
// Update
tasks.updateOne({
update: {
id: '1',
changes: { completed: true },
},
});
// Replace
tasks.setOne({
entity: {
id: '1',
title: 'Review and merge code',
completed: true,
priority: 'high',
},
});
// Delete
tasks.removeOne({ id: '1' });
// Replace all
tasks.setAll({
newEntities: [
{ id: '2', title: 'New task', completed: false, priority: 'medium' },
],
});Using with queryParam
import { queryParam, insertEntities, addOne, removeOne } from '@craft-ng/core';
const filters = queryParam(
{
state: {
selectedIds: {
fallbackValue: [] as string[],
parse: (value) => value.split(',').filter(Boolean),
serialize: (value) => (value as string[]).join(','),
},
},
},
insertEntities({
methods: [addOne, removeOne],
path: 'selectedIds',
}),
);
// Methods update queryParam state and URL
filters.selectedIdsAddOne({ entity: 'item-1' });
// URL: ?selectedIds=item-1
filters.selectedIdsAddOne({ entity: 'item-2' });
// URL: ?selectedIds=item-1,item-2
filters.selectedIdsRemoveOne({ id: 'item-1' });
// URL: ?selectedIds=item-2Best Practices
✅ Use appropriate methods - Choose utilities that match your use case (add vs set vs upsert) ✅ Leverage default identifier - Use objects with id property when possible ✅ Type safety - Let TypeScript infer entity types from your state ✅ Combine with other insertions - Works seamlessly with other insertion functions ✅ Path for nested data - Use path parameter for complex state shapes
❌ Don't overload methods - Only include utilities you actually need ❌ Avoid deep nesting - Consider flattening state if path becomes too complex ❌ Don't mutate directly - Always use the generated methods
Type Safety
insertEntities provides full type inference:
interface Product {
id: string;
name: string;
price: number;
}
const products = state(
[] as Product[],
insertEntities({
methods: [addOne, updateOne],
}),
);
// ✅ TypeScript knows entity must be Product
products.addOne({ entity: { id: '1', name: 'Item', price: 100 } });
// ❌ TypeScript error - missing required properties
products.addOne({ entity: { id: '1' } });
// ✅ TypeScript knows changes are Partial<Product>
products.updateOne({
update: { id: '1', changes: { price: 120 } },
});
// ❌ TypeScript error - invalid property
products.updateOne({
update: { id: '1', changes: { invalid: true } },
});Related
- state - Base primitive for state
- query - Query primitive for async data
- queryParam - URL state synchronization
- entities-util - Entity manipulation utilities
- insertLocalStorage - Persist state
- Store - Compose insertions in stores