Skip to content

Add createQuery and createMutation, replacing solid-query - #284

Merged
pathscale merged 2 commits into
masterfrom
feat/data-primitives-replacing-tanstack
Sep 2, 2026
Merged

Add createQuery and createMutation, replacing solid-query#284
pathscale merged 2 commits into
masterfrom
feat/data-primitives-replacing-tanstack

Conversation

@pathscale

Copy link
Copy Markdown
Owner

First step of removing @tanstack/solid-query from every app. This adds the thing there is currently nowhere to migrate to.

Why

The audit across ~/code: 308 files, ~1000 call sites, ten sites, all @tanstack/solid-query@6.0.0-rc.1. There is zero @tanstack/solid-table anywhere — the grid side is already on this library. It is only the data layer that was never ported.

Nothing in UI/src/hooks offered a replacement (date, form, layout, table), and Solid 2 core does not ship createResource or createAsync — its async surface is createProjection, isPending, latest, onSettled, action. So "remove TanStack" first required writing what replaces it.

The one deliberate behaviour difference

A query that has not run is not pending, and reading it never suspends.

TanStack parks a query that has never fetched — including one held back by enabled: false — at status: "pending". Reading a pending query under Solid 2 throws NotReadyError to suspend, so a permanently-disabled query suspends for the lifetime of the page. NotReadyError extends Error calls super() with no message and does not override name, so a boundary that catches one has nothing to print.

That combination took down every authenticated route of honey.id behind a red "An error occurred" over a blank line. The actual fault was a chat button whose support.cafe queries had never been allowed to run.

Here data() is undefined until there is data, isLoading() is true only while a fetch is in flight, and neither ever throws. A caller that wants to suspend can do so explicitly; a caller that forgets cannot take the page with it.

API

const users = createQuery(() => ({
  key: ["users", page()],
  fetcher: () => api.ListUsers({ page: page() }),
  enabled: isConnected(),
}));

users.data();       // T | undefined
users.isLoading();  // in flight only
users.isReady();    // has produced a value
users.error();      // last failure
users.refetch();

const create = createMutation(() => ({
  mutationFn: (name: string) => api.CreateApp({ name }),
  onSuccess: () => invalidateQueries(["apps"]),
}));

create.mutate("thing");        // failure lands on create.error()
await create.mutateAsync("x"); // rejects, for callers that handle it

The options-thunk shape matches TanStack's Solid signature deliberately, so the migration of ~1000 call sites is mechanical rather than a rewrite.

invalidateQueries(prefix) replaces useQueryClient().invalidateQueries({ queryKey }) and matches by key prefix the same way. It is a plain function rather than something read from context, because invalidation is usually wanted from a mutation handler or a store, neither of which is a component.

Verification

  • bun test --conditions=browser src/hooks/data — 7 pass. The first test is the regression that started this: a disabled query is not loading, never fetches, and never throws.
  • bunx tsc --noEmit clean.
  • bun run lint on the changed files is clean. The repo-wide bun run lint exits 1 on master too — pre-existing, and I checked the baseline rather than assuming. Its fixer also rewrote Calendar.layout.tsx and InputOTP.layout.tsx, which my change never touched; those were reverted and are not in this diff.

Notably not covered by tests: concurrent-key races beyond the generation guard, and behaviour under Loading boundaries. Both are worth exercising once a real app is migrated onto this.

What follows

  1. this PR — the primitive
  2. HoneyAuth panel in this library, so login stops being hand-rolled per app
  3. honey.id off TanStack (44 files) onto this
  4. the remaining nine sites, largest last

meh added 2 commits September 2, 2026 15:48
Every application here reads its data through `@tanstack/solid-query` -- 308
files and roughly a thousand call sites across ten sites -- and nothing in this
library offers an alternative, so there has been nowhere to migrate to. This is
that alternative.

It is not a like-for-like port. One behaviour is deliberately different, and it
is the reason for writing this rather than wrapping the old one:

  A query that has not run is not pending, and reading it never suspends.

TanStack parks a query that has never fetched -- including one held back by
`enabled: false` -- at `status: "pending"`. Reading a pending query under Solid
2 throws `NotReadyError` to suspend, so a query that is disabled suspends for
the lifetime of the page. `NotReadyError` extends `Error` with no message and
does not override `name`, so a boundary catching one has nothing to print. That
combination took every authenticated route of an application down behind a
blank error page, and the cause was a chat button whose queries had never been
allowed to run.

Here `data()` is undefined until there is data, `isLoading()` is true only
while a fetch is in flight, and neither ever throws. A caller that wants to
suspend can; a caller that forgets cannot take the page with it.

`invalidateQueries` is a plain function rather than something read from
context, because invalidation is usually wanted from a mutation handler or a
store, neither of which is a component.
Inferred, it is the union of `render`'s `JSX.Element` and `formatCell`'s
`string`, and naming that union in a declaration needs `RenderedElement` --
internal to `solid-js`, with no importable path from here. That is TS2883.

Whether the compiler reaches for that name depends on how `solid-js` is
hoisted, so the file type-checks against a local `node_modules` and fails
against CI's clean install. The annotation removes the dependency on that
shape rather than papering over the difference.
@pathscale
pathscale force-pushed the feat/data-primitives-replacing-tanstack branch from 0da1d90 to 670b040 Compare September 2, 2026 08:49
@pathscale
pathscale merged commit 4b61908 into master Sep 2, 2026
1 check passed
@pathscale
pathscale deleted the feat/data-primitives-replacing-tanstack branch September 2, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant