Skip to content
Back to skills

Tanstack Query

ASecurity

Operately frontend data fetching with TanStack Query. Use when adding or changing page loaders, model hooks, Api.* calls, mutations, useLoadedData, Pages.useRefresh, or any web UI backend request. New code must use TanStack. When fixing or extending an existing surface, migrate that surface's API calls to TanStack in the same change.

  • 562 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 3, 2026
developmentgoapifrontendbackend

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 24, 2026

npx -y skills add operately/operately --skill tanstack-query --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Tanstack Query?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Tanstack Query
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/operately-tanstack-query/badge)](https://www.skillsdirectory.com/skills/operately-tanstack-query)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: tanstack-query
description: >-
  Operately frontend data fetching with TanStack Query. Use when adding or
  changing page loaders, model hooks, Api.* calls, mutations, useLoadedData,
  Pages.useRefresh, or any web UI backend request. New code must use TanStack.
  When fixing or extending an existing surface, migrate that surface's API
  calls to TanStack in the same change.
---

# TanStack Query

All new web-app backend requests go through TanStack Query. Imperative
`Api.foo.bar()` in loaders, generated tuple hooks (`Api.foo.useBar()`), and
`Pages.useRefresh()` are the old pattern.

When you add a feature or fix on an existing page, hook, or model module,
migrate **that surface's** queries and mutations to TanStack in the same
change. Do not leave a mixed loader (one TanStack query plus one raw `Api.*`
fetch) on the file you just edited.

For copy-paste skeletons and old→new mappings, see [reference.md](reference.md).

## When to migrate

| Situation | Do |
| --- | --- |
| New page, loader, or mutation | TanStack from the start |
| Feature or fix on an existing page/module | Migrate that surface's API calls too |
| Typeahead / search-as-you-type (`People.usePeopleSearch`, `Api.*.search` as a search fn) | Leave imperative |
| Unrelated sibling page | Do not expand the PR |
| `ProjectPage` | Only when that page is the requested work |

`ProjectPage` is the last project-page migration. A one-line copy fix there
does not require rewriting its loader.

## How it works

Generated helpers live next to each endpoint in `app/assets/js/api/index.tsx`:

| Helper | Role |
| --- | --- |
| `fooQuery(input)` | Prefetch in the router loader (`staleTime: Infinity`) |
| `fooQueryOptions(input)` | `queryKey` + `queryFn` for `useQuery` / `useLoadedQuery` |
| `fooQueryKey(input)` | Invalidate one cached input |
| `fooQueryKeyPrefix()` | Invalidate every cached input for that endpoint |
| `fooMutationOptions()` | `mutationFn` for `useMutation` |

The shared client is `app/assets/js/api/queryClient.ts`.

```mermaid
flowchart LR
  loader["router loader: fooQuery"] --> cache["TanStack cache"]
  cache --> hook["useLoadedQuery / useQuery"]
  mutate["mutateAsync"] --> invalidate["invalidateQueries"]
  invalidate --> cache
```

## Page loaders

Router loader **prefetches** and returns **inputs**, not payload:

1. Build `queryInput` (same shape the API already used).
2. `await Api.namespace.fooQuery(queryInput)` (parallelize with `Promise.all`).
3. `return { queryInput }`.
4. `useLoadedData` reads `Pages.useLoadedData()`, then
   `useLoadedQuery(Api.namespace.fooQueryOptions(queryInput))`.
5. Check the declared types: do not assert fields that are already non-nullable.
   For nullable data, prefer safe defaults (for example, `items ?? []` or
   `permissions?.canCreateSpace ?? false`), or hide optional UI when valid.
   Use `assertPresent(value, message)` only for data that may be absent but is
   essential and has no safe fallback, rather than an inline null check that throws.

Use `useLoadedQuery`, not `useQuery`, when the loader prefetched. It uses
`loaderBackedQueryOptions` so the page does not refetch on mount unless the
query was invalidated.

Replace `Pages.useRefresh()` with a local `useRefresh` that
`invalidateQueries` on the page's query keys.

Canonical: [ProjectPausePage/loader.tsx](../../../app/assets/js/pages/ProjectPausePage/loader.tsx),
[ProjectDiscussionPage/loader.tsx](../../../app/assets/js/pages/ProjectDiscussionPage/loader.tsx).

Optional queries (URL may omit space/goal, or a parent fetch may fail): always
call `useLoadedQuery`, pass `enabled: input != null`, and fall back in JS.
See [reference.md](reference.md).

## Mutations

Put wrappers in `app/assets/js/models/<resource>/<resource>Lifecycle.ts` (or
`projectDiscussionLifecycle.ts` when the resource already has a sibling file).

```ts
export function useCreateProjectDiscussion() {
  const queryClient = useQueryClient();

  return useMutation({
    ...Api.projects.createDiscussionMutationOptions(),
    onSuccess: () => {
      void invalidateProjectDiscussionQueries(queryClient);
    },
  });
}
```

Pages call `mutateAsync`. Invalidate with `*QueryKeyPrefix()` so every cached
input for that endpoint refreshes. Re-export from the model's `index.tsx`.

Do not switch every remaining call site of a generated tuple hook when you add
a lifecycle wrapper. Update the surface you are on; leave others (for example
WorkMap's `Api.projects.useCreate()`) until that file is migrated.

Canonical: [projectDiscussionLifecycle.ts](../../../app/assets/js/models/projects/projectDiscussionLifecycle.ts),
[projectLifecycle.ts](../../../app/assets/js/models/projects/projectLifecycle.ts).

## Layout and non-prefetched queries

If the query is **not** prefetched in a router loader (company layout `getMe`),
wrap with `useQuery(fooQueryOptions(input))`, not `useLoadedQuery`.

Canonical: [models/people/index.tsx](../../../app/assets/js/models/people/index.tsx) `useGetMe`.

## Tests

Colocate Jest next to the lifecycle file (`fooLifecycle.test.ts`). Seed
`queryClient.setQueryData(key, {})`, run the invalidate helper, assert
`getQueryState(key)?.isInvalidated`. Cover the intended prefixes and one
unrelated key that must stay clean.

Run `make test FILE=assets/js/models/.../fooLifecycle.test.ts`.

Existing feature tests for the page are the behavior net; run the ones that
visit the migrated route.

## Do not

- Prefetch with raw `Api.foo.bar(input)` or `Projects.getProject(...)`.
- Return fetched records from the loader (`return { project }`). Return inputs.
- Use `Pages.useRefresh()` after a TanStack loader — it will not update cache.
- Introduce `PageCache.fetch` on new work.
- Extract a shared loader helper for two similar pages unless duplication is
  already painful. Include flags and parent APIs usually differ.
- Use `!` to bypass missing query data, or `assertPresent` for non-nullable fields
  or data with a safe fallback.

## Navigation and hover preloading

- `pageRoute` runs `route.loader` for navigation: authentication, progress,
  synchronous `onNavigate`, then the page loader. Hover/focus runs only
  `handle.dataLoader` after 150 ms; the shared company loader stays navigation-only.
- Keep page loaders read-only and reuse the same generated query inputs/options
  in `useLoadedQuery`. Use `emptyLoader` when no data is needed. Put navigation-only
  effects in synchronous `onNavigate`; never change headers in a preloadable loader.
- `auth` defaults to `true`. Set `preload: false` for mutating reads, redirects
  with browser effects, context changes, and mandatory fresh checks. Do not relax
  freshness or company isolation for preloading. Individual links can opt out with
  `data-preload="false"`; cross-company links are skipped automatically.
- Cached-query transport has no toast/reload effects. Navigation and active query
  observers report errors centrally. Imperative cached queries without an observer
  keep local error handling; delayed stale-client detection is an accepted tradeoff.
- See the [minimal route example](reference.md#navigation-and-preloading).

Files in this skill

  • SKILL.md5.4 KB
  • reference.md4.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.