Skip to content

query

The query primitive manages server data fetching - that can be easily extended for syncing with localStorage, reacting to mutations (that unlock optimistic update, update, reload on failed...).

Import

typescript
import { query } from '@ng-angular-stack/craft';

Basic Examples

Params-based query

typescript
const myQuery = query({
  params: { id: 1 },
  loader: async ({ params }) => {
    const response = await fetch(`/api/users/${params.id}`);
    return response.json();
  },
});

// Access query state
console.log(myQuery.value()); // User data
console.log(myQuery.isLoading()); // true/false
console.log(myQuery.error()); // Error or undefined

Identifier-based queries (for parallel queries)

typescript
const userId = signal<number | undefined>(undefined);
const query = query({
  params: userId,
  identifier: (id) => id,
  loader: async ({ params: userId }) => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  },
});

// Both queries run in parallel
userId.set(1);
// later
userId.set(2);
// Once all queries are resolved
console.log(query.select('1').value()); // User 1 data
console.log(query.select('2').value()); // User 2 data

React to mutation with insertReactOnMutation and persist in local storage

typescript
import { insertReactOnMutation } from '@ng-angular-stack/craft';

const updateUserMutation = mutation({
  method: (data: { id: string; name: string; email: string }) => data,
  loader: async ({ params }) => {
    const response = await fetch(`/api/users/${params.id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(params),
    });
    return response.json();
  },
});

const userQuery = query(
  {
    params: () => ({ userId: currentUserId() }),
    loader: async ({ params }) => {
      const response = await fetch(`/api/users/${params.userId}`);
      return response.json();
    },
  },
  insertReactOnMutation(updateUserMutation, {
    // Optimistically update while mutation is loading
    optimisticPatch: {
      name: ({ mutationParams }) => mutationParams.name,
      email: ({ mutationParams }) => mutationParams.email,
    },
    // Reload the query if updateUserMutation failed
    reload: { onMutationError: true },
  }),
  insertLocalStoragePersister({
    storeName: 'demo-app',
    key: 'user-query',
  }),
);

// When mutation is triggered, query updates immediately (optimistic)
updateUserMutation.mutate({
  id: '123',
  name: 'New Name',
  email: 'new@email.com',
});
// userQuery.value() is updated optimistically

// When mutation completes, patch confirms the change

Important Notes

⚠️ Injection Context: This function must be called within an injection context. If called outside, it will only return an object containing the configuration under _config.

Query with insertions for custom methods

typescript
const todosQuery = query(
  {
    params: () => ({ completed: showCompleted() }),
    loader: async ({ params }) => {
      const response = await fetch(`/api/todos?completed=${params.completed}`);
      return response.json();
    },
  },
  ({ value, isLoading }) => ({
    count: computed(() => value()?.length ?? 0),
    isEmpty: computed(() => !isLoading() && value()?.length === 0),
  }),
);

// Access custom computed properties
console.log(todosQuery.count()); // Number of todos
console.log(todosQuery.isEmpty()); // true/false

Preserve previous value to avoid flickering

typescript
const postsQuery = query({
  params: () => ({ page: currentPage() }),
  preservePreviousValue: () => true, // Keep showing old data while loading
  loader: async ({ params }) => {
    const response = await fetch(`/api/posts?page=${params.page}`);
    return response.json();
  },
});

// When page changes, old data remains visible until new data loads

Best Practices

Use preservePreviousValue to avoid flickering during navigation ✅ Use insertions to add custom computed properties and methods

See Also