npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

define-query

v5.0.0

Published

Define TanStack Query options once — queries, mutations, optimistic updates, and row state.

Downloads

1,483

Readme

define-query

Тонкий хелпер без залежностей, який дозволяє визначити запити й мутації один раз і використовувати їх із рідними хуками TanStack Query.

Він не обгортає й не підміняє useQuery / useMutation. Натомість:

  • defineQuery / defineInfiniteQuery → фабрики, що повертають рідні queryOptions / infiniteQueryOptions.
  • defineMutation → фабрика, що повертає рідні mutationOptions із вшитими draft-апдейтами, звіркою temp-id, rollback і синхронізацією сусідніх запитів.

English


Ментальна модель

defineQuery(config)            →  (params) => queryOptions(...)
defineInfiniteQuery(config)    →  (params) => infiniteQueryOptions(...)
defineMutation(config)           →  (params) => mutationOptions(...)

Уся поверхня TanStack лишається твоєю (isPending, error, fetchStatus, mutate, mutateAsync, …). Ліба лише будує об'єкти опцій для рідних хуків.

| Шар | Відповідальність | |-----|------------------| | Query | key, fetch, опційний spread, опційні options (проксяться в TanStack) | | Mutation | request, опційний validate, одна draft-форма, опційний sync | | Draft-форма | object, insertInto / prependInto, updateIn, removeFrom або dropQuery | | Spread / sync | Query spread після успішного network fetch; mutation sync після успішного request | | UI state | Рідний useMutation і useQuery (draft у кеші) |

Опційне поле query у defineMutationякий кеш оновлює мутація, а не обов'язково endpoint API. addCommentpostCommentsQuery, renamePostpostQuery тощо. Без query — тонка мутація (request + опційний sync).


Налаштування

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

<QueryClientProvider client={queryClient}>
  <App />
</QueryClientProvider>

define-query підключається до QueryClient автоматично: при першому network fetch query з spread і при першій мутації. Ручний bootstrap не потрібен.

Зарезервований meta: не задавай options.meta['define-query'] на query-фабриках — цей ключ належить бібліотеці.


Швидкий старт

import { useMutation, useQuery } from '@tanstack/react-query';
import { defineMutation, defineQuery } from 'define-query';

const postQuery = defineQuery({
  key: (id: string) => ['post', id] as const,
  fetch: (id) => api.getPost(id),
  options: { staleTime: 30_000 },
});

const renamePost = defineMutation({
  query: postQuery,
  name: 'rename',
  request: (id: string, title: string) => api.patchPost(id, { title }),
  draft: ({ data, input }) => ({ ...data, title: input }),
  sync: (on) => [
    on(timelineQuery).mergeIn('items', { set: (_item, { input }) => ({ title: input }) }),
  ],
});

function Post({ id }: { id: string }) {
  const { data } = useQuery(postQuery(id));
  const rename = useMutation(renamePost(id));

  if (!data) return null;
  return (
    <input
      defaultValue={data.title}
      onBlur={(e) => rename.mutate(e.target.value)}
    />
  );
}

Ключі

Використовуй factory.key(params) — нормалізований стабільний ключ:

postQuery.key(id);              // те саме, що postQuery(id).queryKey
addComment.key(postId);         // те саме, що addComment(postId).mutationKey

queryClient.invalidateQueries({ queryKey: postQuery.key(id) });
useIsMutating({ mutationKey: addComment.key(postId) });

Запити

defineQuery

Params мають бути плоским об'єктом або скаляром. Вкладені структури згортаються в {} з console warning у будь-якому середовищі.

Для nested filter state (таблиці, складний пошук) — serializeParams: перетворює params лише для keying; fetch отримує raw params:

const tableQuery = defineQuery({
  serializeParams: (p) => ({
    filterKey: stableFilterKey(p.filter),
    page: p.page,
  }),
  key: (k) => ['table', k] as const,
  fetch: (p) => api.list(p),
});

Якщо output serializeParams лишається вкладеним — console warning; flatten/stringify у util. Bound mutation успадковує hook від query; name-only mutation може мати власний serializeParams.

queryClient.invalidateQueries({ queryKey: postCommentsQuery.key(postId) });

defineInfiniteQuery

const items = flattenInfiniteField(timeline.data, 'items');

Query spread

spread на defineQuery / defineInfiniteQuery — після кожного успішного network fetch (не після ручного setQueryData). Підключається автоматично при першому fetch. Повторюється на refetch, prefetch і invalidate.

| Операція | Ефект | |----------|-------| | on(query).set(...) | Upsert одного sibling entry | | on(query).setEach(field, ...) | Upsert для кожного елемента списку |

Mutation sync — окремо: після успішного request, інший набір ops (bump, mergeIn, removeFrom, set, invalidate). Той самий синтаксис on(query), різні тригери.


Мутації

defineMutation(config) повертає (params) => mutationOptions. Обов'язковий унікальний name. Обирай одну draft-форму, коли є query.

| query | mutationKey | |---------|----------------| | заданий | [...queryKey, name] | | відсутній | [name] або [name, params] |

query обов'язковий для draft-форм. Для лише request + sync — можна опустити.

Тонка мутація (без query)

const reportSpam = defineMutation({
  name: 'reportSpam',
  request: (postId: string, reason: string) => api.report(postId, reason),
  sync: (on) => [on(postQuery).invalidate({ params: ({ params }) => params })],
});

DraftCtx — єдиний API для draft

type DraftCtx<TData, TInput> = {
  data: TData;
  input: TInput;
  tempId?: string;  // insert/prepend
  item?: TItem;     // update — знайдений рядок
};

| Форма | draft | settle | |-------|---------|----------| | object | (ctx) => TData | (ctx & { response }) => TData | | insertInto / prependInto | (ctx) => TItem | (response) => TItem | | update | (ctx) => Partial<TItem> | (ctx & { response }) => Partial<TItem> | | removeFrom / dropQuery | — | — |

Приклади

// object
draft: ({ data, input }) => ({ ...data, title: input }),

// insert
draft: ({ input, tempId }) => ({ id: tempId!, text: input }),

// update
draft: ({ input }) => ({ text: input.text }),

Infinite: insertInto — остання сторінка, prependInto — перша. Порожній pages bootstrap'иться першою сторінкою з елементом.

Без кешу: якщо даних у кеші ще немає — draft пропускається (console warning), виконується лише request.

List paths

List-мутації (insertInto / prependInto / updateIn / removeFrom) і sync list ops приймають:

| Форма кешу | Синтаксис | Приклад | |------------|-----------|---------| | Об'єкт з масивом (top-level або nested) | dot-path string | 'items', 'response.nodes' | | Root-масив (TData = Post[]) | true | prependInto: true |

Шляхи перевіряються TS — невалідні ключі або non-array targets дають compile error. Обраний path звужує тип елемента в draft / match / settle.

// nested list (GraphQL-style)
defineMutation({
  query: graphCommentsQuery,
  name: 'add',
  request: (id, text) => api.add(id, text),
  insertInto: 'response.nodes',
  draft: ({ input, tempId }) => ({ id: tempId!, text: input }),
});

// API повертає Post[] без обгортки
defineMutation({
  query: postsQuery,
  name: 'create',
  request: (_, input) => api.create(input),
  prependInto: true,
  draft: ({ input, tempId }) => ({ id: tempId!, title: input.title }),
});

Той самий синтаксис — для mergeIn, removeFrom, setEach, flattenInfiniteField. bump приймає dot-path до вкладених числових полів (без true).

Конкурентність

Паралельні мутації на одному списку безпечні: rollback по rowId / tempId. Два edit одного рядка — останній успішний response виграє при settle.


Валідація

validate(input) виконується до draft — fail.validation(...) відхиляє мутацію без змін у кеші. Той самий fail.validation(...) можна кинути з request (наприклад, 422 від API); у разі будь-якої помилки draft відкочується.

Кожне поле — string або string[]; .field(key) повертає перше повідомлення.

Клієнтська — validate

const createPost = defineMutation({
  query: timelineQuery,
  name: 'create',
  request: (_params, { title, body }) => api.createPost({ title, body }),
  validate: ({ title, body }) => {
    if (!title.trim()) throw fail.validation({ title: ['Заголовок не може бути порожнім'] });
    if (!body.trim()) throw fail.validation({ body: ['Текст не може бути порожнім'] });
  },
  prependInto: 'items',
  draft: ({ input, tempId }) => ({ id: tempId!, title: input.title, body: input.body }),
});

Працює і без query — для тонких мутацій, де потрібні лише перевірки форми:

const reportSpam = defineMutation({
  name: 'reportSpam',
  validate: reason => {
    if (!reason.trim()) throw fail.validation({ reason: 'Обовʼязкове поле' });
  },
  request: (postId, reason) => api.report(postId, reason),
});

Серверна — request

request: async (postId, text) => {
  const res = await api.addComment(postId, text);
  if (res.error) throw fail.validation(res.error); // напр. { text: ['Занадто коротко'] }
  return res;
},

UI — поле vs banner

function CreateForm({ params }: { params: { q: string } }) {
  const create = useMutation(createPost(params));

  function submit(e: React.FormEvent) {
    e.preventDefault();
    create.reset(); // скинути попередні помилки
    create.mutate({ title, body });
  }

  const titleError = create.error?.field('title'); // лише validation
  const bodyError = create.error?.field('body');
  const banner = create.error?.banner(); // network / server — null для validation

  return (
    <form onSubmit={submit}>
      {titleError && <span>{titleError}</span>}
      {bodyError && <span>{bodyError}</span>}
      {banner && <span>{banner}</span>}
    </form>
  );
}

Для одного рядка вводу — field('text') і banner() покривають validation і мережеві помилки:

<>
  {add.error?.field('text') && <span>{add.error.field('text')}</span>}
  {add.error?.banner() && <span>{add.error.banner()}</span>}
</>

Помилки

import { fail } from 'define-query';

throw fail.validation({ text: ['Порожньо'] });
throw fail.network('Offline');
throw fail.server('Помилка сервера');

mutation.errorDefineQueryMutationError:

add.error?.field('text');  // поле форми
add.error?.banner();       // toast/banner (не validation)

isMutationError — лише для unknown (спільні helpers).


Mutation sync

Після успішного request. Див. таблицю ops у README.md.


Утиліти

| Експорт | Призначення | |---------|-------------| | factory.key(params) | Стабільний ключ query / mutation | | factory.with(params, overrides) | Per-call TanStack options поверх factory (queries і mutations) | | flattenInfiniteField(data, field) | Сплющити список по infinite-сторінках | | getPendingInput / matchPending | In-flight input мутації | | remapIds(client, input) | Temp → server id у ручних cache writes | | isTempId / createTempId | Temp id | | isMutationError | Type guard для помилки мутації | | error.display({ fields?, fallback? }) | Перше повідомлення для UI | | fail | validation / network / server | | QueryParams<TQuery> | Infer params для typed sync/spread params |


Живі приклади: ../define-query-demo/src/demo/queries/, ../define-query-demo/src/demo/feed/, ../define-query-demo/src/demo/thread/.