Skip to main content

createQueryGroupCRUD

The core factory function that generates a complete set of CRUD operations with proper query keys, invalidation logic, and normalization functions.

Type Signature

export const createQueryGroupCRUD = <T = string>(
entityName: string,
): QueryGroupCRUD<T>

Parameters

entityName

  • Type: string
  • Required: Yes
  • Description: The name of the entity (e.g., 'accounts', 'transactions', 'users')

T (Generic Type)

  • Type: Type parameter
  • Default: string
  • Description: The type of the entity's ID (e.g., string, number, Account['id'])

Return Type

interface QueryGroupCRUD<T> {
all: QueryGroup<T>;
list: QueryGroup<T>;
detail: QueryGroupResolved<T>;
create: QueryGroup<T>;
update: QueryGroupMutationResolved<T>;
remove: QueryGroupMutationResolved<T>;
}

Generated Operations

all

Represents all entities without filtering.

{
queryKey: { entity: entityName }
}

Use case: Invalidating or canceling all queries for an entity.

list

Represents a list/collection query.

{
queryKey: { entity: entityName, method: 'list' },
type: 'query'
}

Use case: Fetching all records, with optional filtering/pagination in wrapper hooks.

detail

Represents fetching a single entity by ID.

{
queryKey: (id: T) => ({ entity: entityName, method: 'detail', id }),
type: 'query',
normalize: (data) => {
// Updates the item in the list cache
queryClient.setQueryData([list.queryKey], (old) => {
if (!old) return old;
return old.map((item) => (item.id === data.id ? data : item));
});
}
}

Features:

  • Dynamic key: Function that takes an ID
  • Normalization: Automatically updates the list cache when detail data changes

create

Represents creating a new entity.

{
queryKey: { entity: entityName, method: 'create' },
invalidates: { entity: entityName, method: 'list' },
type: 'mutation',
normalize: (data) => {
// Add new item to list cache
queryClient.setQueryData([list.queryKey], (old) => {
if (!old) return [data];
return [...old, data];
});
// Set detail cache
queryClient.setQueryData([detail.queryKey(data.id)], data);
}
}

Features:

  • Invalidates: List queries automatically
  • Normalization: Optimistically adds new item to cache

update

Represents updating an existing entity.

{
queryKey: (id: T) => ({ entity: entityName, method: 'update', id }),
invalidates: (id: T) => [
{ entity: entityName, id },
{ entity: entityName, method: 'list' }
],
type: 'mutation',
normalize: (data) => {
// Update item in list cache
queryClient.setQueryData([list.queryKey], (old) => {
if (!old) return old;
return old.map((item) => (item.id === data.id ? data : item));
});
// Update detail cache
queryClient.setQueryData([detail.queryKey(data.id)], data);
}
}

Features:

  • Dynamic key: Function that takes an ID
  • Invalidates: Both the specific entity and the list
  • Normalization: Updates both list and detail caches

remove

Represents deleting an entity.

{
queryKey: (id: T) => ({ entity: entityName, method: 'remove', id }),
invalidates: (id: T) => [
{ entity: entityName, id },
{ entity: entityName, method: 'list' }
],
type: 'mutation',
normalize: (data) => {
// Remove item from list cache
queryClient.setQueryData([list.queryKey], (old) => {
if (!old) return old;
return old.filter((item) => item.id !== data.id);
});
// Clear detail cache
queryClient.setQueryData([detail.queryKey(data.id)], undefined);
}
}

Features:

  • Dynamic key: Function that takes an ID
  • Invalidates: Both the specific entity and the list
  • Normalization: Removes item from list cache and clears detail cache

Basic Example

import { createQueryGroupCRUD } from 'src/queries';

// Create a basic CRUD query group
const usersQueryGroup = createQueryGroupCRUD('users');

// Use the generated operations
console.log(usersQueryGroup.list.queryKey);
// { entity: 'users', method: 'list' }

console.log(usersQueryGroup.detail.queryKey('123'));
// { entity: 'users', method: 'detail', id: '123' }

console.log(usersQueryGroup.update.invalidates('123'));
// [
// { entity: 'users', id: '123' },
// { entity: 'users', method: 'list' }
// ]

With TypeScript Types

import { createQueryGroupCRUD } from 'src/queries';
import { Account } from 'src/generated';

// Use the entity's ID type for type safety
const accountsQueryGroup = createQueryGroupCRUD<Account['id']>('accounts');

// Now TypeScript enforces the correct ID type
accountsQueryGroup.detail.queryKey('string-id'); // ✓ OK if Account['id'] is string
accountsQueryGroup.detail.queryKey(123); // ✗ Error if Account['id'] is string

Real-World Example

import { createQueryGroupCRUD, inyectKeysToQueries } from 'src/queries';
import { Transaction } from 'src/generated';

// Step 1: Create base CRUD operations
let transactionsQueryGroupCRUD = createQueryGroupCRUD<Transaction['id']>('transactions');

// Step 2: Inject authentication context
transactionsQueryGroupCRUD = inyectKeysToQueries(transactionsQueryGroupCRUD, {
auth: true,
});

// Step 3: Export with any custom extensions
export const transactionsQueryGroup = {
...transactionsQueryGroupCRUD,
// You can override or extend specific operations here
};

Extending Generated Operations

You can override or extend any generated operation:

const accountsQueryGroupCRUD = createQueryGroupCRUD<Account['id']>('accounts');

export const accountsQueryGroup = {
...accountsQueryGroupCRUD,

// Override remove to cascade invalidations
remove: {
...accountsQueryGroupCRUD.remove,
invalidates: (id: Account['id']) => [
...accountsQueryGroupCRUD.remove.invalidates(id),
transactionsQueryGroup.all.queryKey,
movementsQueryGroup.all.queryKey,
],
},

// Add custom operations
archive: {
queryKey: (id: Account['id']) => ({ entity: 'accounts', method: 'archive', id }),
invalidates: (id: Account['id']) => [
{ entity: 'accounts', id },
{ entity: 'accounts', method: 'list' },
],
type: 'mutation',
},
};

Normalization Behavior

All mutations include a normalize function that provides optimistic updates:

Create Normalization

normalize: (data: { id: any }) => {
// Add to list
queryClient.setQueryData([list.queryKey], (old: any) => {
if (!old) return [data];
return [...old, data];
});
// Set detail
queryClient.setQueryData([detail.queryKey(data.id)], data);
}

Update Normalization

normalize: (data: { id: any }) => {
// Update in list
queryClient.setQueryData([list.queryKey], (old: any) => {
if (!old) return old;
return old.map((item: any) => (item.id === data.id ? data : item));
});
// Update detail
queryClient.setQueryData([detail.queryKey(data.id)], data);
}

Remove Normalization

normalize: (data: { id: any }) => {
// Remove from list
queryClient.setQueryData([list.queryKey], (old: any) => {
if (!old) return old;
return old.filter((item: any) => item.id !== data.id);
});
// Clear detail
queryClient.setQueryData([detail.queryKey(data.id)], undefined);
}

Integration with Wrapper Hooks

The generated query groups are designed to work seamlessly with wrapper hooks:

// Query wrapper
export const useTransactions = () =>
generatedTransactions({
query: { queryKey: [transactionsQueryGroup.list.queryKey] },
});

// Mutation wrapper with normalization
export const useTransactionCreate = ({ onSuccess, ...rest }) =>
generatedTransactionCreate({
mutation: {
mutationKey: [transactionsQueryGroup.create.queryKey],
onSuccess: (data, variables, context) => {
// Call normalize for optimistic update
transactionsQueryGroup.create.normalize?.(data);

// Invalidate related queries
queryClient.invalidateQueries({
queryKey: [transactionsQueryGroup.create.invalidates],
});

onSuccess?.(data, variables, context);
},
...rest,
},
});

Best Practices

1. Use Typed IDs

Always provide the ID type for type safety:

// GOOD
createQueryGroupCRUD<Account['id']>('accounts');

// AVOID (loses type safety)
createQueryGroupCRUD('accounts');

2. Inject Keys Early

Apply inyectKeysToQueries immediately after creation:

// GOOD
let group = createQueryGroupCRUD<T>(entity);
group = inyectKeysToQueries(group, { auth: true });

// Then extend or export
export const entityQueryGroup = { ...group };

3. Keep Entity Names Consistent

Use plural, lowercase entity names that match your API:

// GOOD
createQueryGroupCRUD('accounts');
createQueryGroupCRUD('transactions');

// AVOID
createQueryGroupCRUD('Account');
createQueryGroupCRUD('TransactionEntity');

4. Don't Modify Normalize Functions Directly

If you need custom normalization, do it in wrapper hooks:

// GOOD - Custom normalization in wrapper hook
export const useAccountUpdate = ({ onSuccess, ...rest }) =>
generatedAccountUpdate({
mutation: {
onSuccess: (data, variables, context) => {
accountsQueryGroup.update.normalize?.(data);

// Custom additional normalization
queryClient.setQueryData([balanceQueryKey], (old) => ({
...old,
total: data.balance,
}));

onSuccess?.(data, variables, context);
},
...rest,
},
});

See Also