Skip to content

useQueryClient / useContext runs inside createMemo, breaking Solid rules and causing undefined QueryClient (e.g. defaultQueryOptions on lazy routes) #10445

@manzettidenis

Description

@manzettidenis

Describe the bug

Hello guys, I am experiencing a weird behavior and investigating a little bit thats what I found:

Several hooks resolve the QueryClient with:

const client = createMemo(() => useQueryClient(queryClient?.()))

useQueryClient (in QueryClientProvider.tsx) calls useContext(QueryClientContext) when no explicit client is passed.

In Solid, useContext is only valid when run in the synchronous, top-level execution of a component / custom hook — not inside a nested reactive scope such as a createMemo callback (which can run later or in a different tracking context).

That mismatch can surface as:

TypeError: Cannot read properties of undefined (reading 'defaultQueryOptions') (or similar) when client() is effectively wrong/undefined during certain navigation / lazy-route / load phases.

Fragile behavior under code-splitting (e.g. lazy-loaded route chunks) where context resolution timing differs.

We hit this in production on SolidStart + file-based routing + lazy routes; we worked around it with a local patch that resolves the context once at hook top level and only uses createMemo for a plain () => QueryClient accessor.

Your minimal, reproducible example

tricky to reproduce, resolving the client via useQueryClient() inside createMemo is not valid for Solid’s context rules

Steps to reproduce

Library code (always reproducible as a pattern violation)

Current main still has the anti-pattern in useBaseQuery:

File: packages/solid-query/src/useBaseQuery.ts
Line ~110: const client = createMemo(() => useQueryClient(queryClient?.()))
The same createMemo(() => useQueryClient(...)) pattern appears in (at least):

useQueries.ts
useMutation.ts
useIsFetching.ts
useIsMutating.ts
useMutationState.ts

**2. App-level runtime repro **

We don’t have a public minimal StackBlitz handy, but the conditions that triggered the bug for us were:

SolidStart (or Vite + Solid Router) with lazy route components (async import()).

App wrapped in QueryClientProvider at the root.

Inside a lazy-loaded route module, call useQuery (or useQueries / useMutation) without passing the optional queryClient accessor, so resolution goes through context.

Navigate to that route (client-side navigation) and exercise the query (mount + suspense / load as applicable).

Observed (intermittent): crash when internal code does client().defaultQueryOptions(...) because client resolved to undefined.

Expected behavior

useContext(QueryClientContext) should run once per hook invocation, at top level of useBaseQuery / useQueries / etc., not inside createMemo.
createMemo should only wrap non-hook work, e.g. reading an optional queryClient accessor and/or calling the context-derived getter.
Conceptually (pseudo-code):

// Top level of the hook (runs synchronously with the component)

const resolveClient = useQueryClientResolver(queryClient) // useContext happens here
const client = createMemo(() => resolveClient())

How often does this bug happen?

Every time

Screenshots or Videos

Image Image

Platform

Platform
@tanstack/solid-query: 5.96.2 (also checked main on GitHub and the pattern is still present)
solid-js: 1.x
Framework: SolidStart / Vinxi

Additional context
Solid’s reactivity model differs from React: useContext is not safe inside arbitrary memos/computations the same way as “run once per render” in React.

Reference (Solid): Rules of hooks / reactive context usage should be read in the proper scope, not hidden inside deferred reactive callbacks.

Tanstack Query adapter

None

TanStack Query version

5.96.2

TypeScript version

5.7.3

Additional context

Short “why reproduce is tricky” note for you
The bug is probably from source (useContext inside createMemo).
The runtime failure can be timing-dependent (lazy chunks, navigation, SSR/hydration), so a one-file repro may take a bit of iteration.

Proposed fix
Introduce a small resolver (e.g. useQueryClientResolver(optionalAccessor)) that:

Runs useContext(QueryClientContext) once at hook top level (or uses the documented pattern for optional injected client).
Returns a stable function () => QueryClient safe to call from memos / observers.
Replace patterns like
createMemo(() => useQueryClient(queryClient?.()))
with
const resolve = useQueryClientResolver(queryClient) and createMemo(() => resolve()) only if the memo body is allowed to call a non-hook resolver (the resolver itself must not call useContext).

Apply the same pattern everywhere the package currently does createMemo(() => useQueryClient(...)) (useQuery, useQueries, useMutation, useIsFetching, useIsMutating, useMutationState, etc.) so behavior is consistent.

Tests:

Unit or integration test that simulates missing or late context vs injected client and asserts no throw when the resolver is used correctly.
If feasible, a test that forbids useContext from being invoked inside a createMemo callback for these hooks (lint or a small runtime dev check is optional; tests are enough for many reviewers).

I maintain a pnpm patch that introduces useQueryClientResolver and replaces createMemo(() => useQueryClient(...)) across the files above; happy to contribute a PR if this direction matches your preferences

Best regards! 🇧🇷 🚀

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions