Hosted ondailyplanet.iovia theHypermedia Protocol

Remix + React Query: why your data looks drunk

    Written by chatGPT5

    You’re trying to let Remix loaders and React Query (RQ) “own” the same data. That splits authority and guarantees flicker, stale props, and double-fetches. Pick a boss.

    Two data models, one UI

      Remix loader: pushes JSON into useLoaderData(). Remix decides when to revalidate (navigations, mutations, focus, etc.).

      React Query: owns a cache, with timestamps, staleTime, invalidations, and optimistic updates.

    Run both on the same slice and you get:

      Stale props: loader data freezes at navigation time while RQ mutates underneath.

      Double fetch: loader runs on navigation; RQ refetches on mount because your query is “stale.”

      Conflicted writes: Remix revalidation repaints old loader data right after an RQ mutation unless you micromanage.

      Mental overhead: two invalidation systems, two lifecycles, zero fun.

    The workable pattern

    If you insist on SSR + RQ, do SSR hydration with RQ and stop reading loader data in the component.

      export function shouldRevalidate() { return false; } (or conditionally).

      Or move volatile bits into resource routes and fetch them via RQ, keeping parent route loaders stable.

    Common faceplants

      Mixing useLoaderData() into UI that also calls useQuery for the same entity.

      Different queryKeys server vs client → hydration mismatch → double fetch.

      No staleTime → instant refetch on mount even though you just dehydrated.

      Using <Form>/actions for mutations while RQ also invalidates → dueling revalidations.

    Why RQ wins post-hydrate

    Remix gives you server data and routing. RQ gives you cache, retries, dedupe, mutations, optimistic UI, background refresh. After first paint, you want RQ in charge—or you’re herding cats.

    Options (pick one, don’t waffle)

      RQ-first (recommended) Dehydrate in loader, read with useQuery only, mutations via RQ, shouldRevalidate=false. Clean, predictable.

      Remix-first Use loaders/fetchers for reads/mutations; drop RQ for that domain. Less power, fewer moving parts.

      Islands split Stable parent via Remix loader; volatile lists/resources via resource routes + RQ. Invalidate only those keys. Minimal blast radius.

      Hybrid with truce Keep both, but on mutation: invalidateQueries() and revalidator.revalidate(). Accept extra fetches and complexity. Your call.