Ask
265
@torque_spec ·

zustand or leave it in the query cache - 2 devs, 40 screens, offline edits Global State

Field-service app. Two of us, about 40 screens, and technicians work in places with no signal, so edits have to survive being made offline and sync later. Right now everything server-shaped lives in TanStack Query and the handful of genuinely client things (filters, the open drawer, a draft form) live in three small Zustand stores.

One of us wants to move the entity data into Zustand so offline edits are just state we own. The other wants to keep it in the query cache and use the persister plus a mutation queue.

With two people and no appetite for building a sync engine, which way is less regret?

10 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @clickertrainbee · 2mo ago · 3 replies

    Keep server data in the query cache. What you are describing - offline edits that sync later - is persistQueryClient plus paused mutations, and that is maybe 60 lines rather than a project.

    The shape:

    • a persister writing the cache to IndexedDB, gcTime bumped well past your persist window or entries get thrown away before they are restored
    • onlineManager flips to offline, mutations queue instead of failing
    • each mutation gets a mutationKey and a registered mutationFn so resumePausedMutations() can replay them after a restart
    • optimistic updates so the technician sees their own edit immediately

    Moving entities into Zustand means you now own fetching, caching, invalidation, deduping, retry and staleness by hand, in a two-person team, for 40 screens. I have done that migration in the other direction and it took a quarter.

    217
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @clickertrainbee · 2mo ago

      On you, and it would be on you in Zustand too - that is a domain decision, not a library feature. Cheapest thing that works: send the updated_at you read with the mutation, have the server reject on mismatch with a 409 and the current row, and surface a "this changed while you were offline" screen. Two of you will not build merge semantics, and technicians would rather be told than silently overwritten.

      78
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @torque_spec · 2mo ago

      The bit I keep tripping on is conflict handling. If a technician edits a job offline and someone edits it in the office, replaying my mutation just clobbers them. Does the paused mutation approach give you anything for that or is it on me?

      61
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @mulch_marta · 2mo ago · 3 replies

    The framing that made this stop being an argument on my team: state is either owned by the server or owned by the client, and the question is never which library, it is which of those two a piece of data is. A job record is server-owned even when you are editing it offline - you are holding a pending intent, not the truth. Drafts, filters and UI state are client-owned.

    Once you say it that way, your current split is already correct and the proposal is to break it. Keep the three Zustand stores small and stop worrying that they are small.

    143
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @mulch_marta · 2mo ago

      Client-owned until submit, and then it becomes a mutation with its own identity - not a mutated copy of the server record. Keep the draft in Zustand keyed by job id, and on submit hand it to a mutation and clear the draft on success. The ugliness usually comes from trying to make one object be both at once.

      64
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @torque_spec · 2mo ago

      Where does a half-finished job form sit in that split? It is client-owned while being typed and server-owned the moment it is submitted, and that transition is where our code gets ugly.

      46
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @sdcard_sid · 2mo ago

    One caution on the persister with 40 screens: serialise-everything-on-every-change will get slow on a mid-range Android tablet. Throttle it, and set a shouldDehydrateQuery that only persists the handful of keys a technician needs offline rather than the entire cache. We persist 6 query prefixes out of about 90 and the rehydrate is under 40ms.

    88
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @schema_drift_lu · 2mo ago

    Slightly against the grain: if the offline story is the product rather than a nice-to-have, neither option is really the answer and you want a local database with sync built in. But that is a much bigger commitment and you would be choosing it because "works offline for a full shift" is what customers pay for, not because the query cache annoyed you. From the post it sounds like a nice-to-have, so ignore me.

    52
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @flashcard_fen · 2mo ago

    Whatever you pick, write the sync failure UI first. We built the queue, tested it on aeroplane mode, shipped it, and discovered that the interesting case is not "no signal" but "two bars of signal, requests take 45 seconds and then time out". That state looks online to every API you have and is where all the real bugs live.

    37
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @pivot_pilot · 2mo ago

    Two devs, 40 screens, offline sync. Whichever you choose, the correct amount of custom sync engine is zero and you will build one anyway, so at least start from the one that already has retries.

    24
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report