Skip to content

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

typescript
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 id property
  • Parallel queries - Enables entity manipulation in query instances with select parameter
  • Type inference - Full TypeScript support with automatic method name generation

Entity Utilities

The following entity utility functions can be used with insertEntities:

UtilityDescription
addOneAdds a single entity to the end
addManyAdds multiple entities to the end
setOneReplaces or adds an entity by ID
setManyReplaces or adds multiple entities by ID
setAllReplaces the entire collection
updateOnePartially updates an entity by ID
updateManyPartially updates multiple entities by ID
upsertOneUpdates if exists, otherwise adds
upsertManyUpdates multiple if exist, otherwise adds
removeOneRemoves a single entity by ID
removeManyRemoves multiple entities by ID
removeAllClears the entire collection

Signature

typescript
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 id property: (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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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-2

Best 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:

typescript
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 } },
});