✨ EFLx ☁️

Back

Some time ago, I wrote Safe & Consistent SWR, which introduced a paradigm for data fetching in pure client-side React applications using SWR or similar libraries. After practicing this pattern for two months, the issues with this paradigm have gradually surfaced, and they deserve a rational look.

The Chaos of the Source of Truth#

As business logic becomes more complex, the number of data “scopes” or “endpoints” increases. You need to handle data and business logic for different domains like users, projects, and posts. This is where the problem emerges: The SWR pattern identifies a specific piece of data through a “Key,” and builds caching, deduplication, and mutations on top of it. However, ten endpoints mean ten key definitions; twenty endpoints mean twenty key definitions. As the scale grows, you start thinking about reducing boilerplate code. Let’s look at the flow:

  1. On the backend, we expose various CRUD interfaces via /api, Electron IPC, or other means.
  2. In the frontend SWR layer, we define the Key, the SWR hook for fetching data, and the SWR hooks for mutating data for every single CRUD interface.
  3. In the frontend components, we use these hooks.

There seems to be nowhere to streamline. Obviously, the SWR layer requires manual effort to establish the relationship between Keys and backend data, manually defining which CRUD operations affect which pieces of data. This is the root of the discomfort.

Ultimately, only the backend knows what a backend operation will change. Therefore, handing the task of “assigning endpoints to data and binding mutation operations to data updates” to the frontend is inherently illogical. The backend is the true source of truth for these mappings.

tRPC? Server Action? GraphQL?#

I first thought of electron-trpc. Perhaps it’s a bit “heretical,” but for general /api servers, tRPC is a viable solution. However, this brings significant refactoring costs and carries risks when switching the backend to a decoupled design (e.g., running a Go or Python service).

What about Server Actions? I must admit, the DX is excellent. In a “full-stack” React app, using primitives like RSC + Server Actions isn’t some monster to be feared. In fact, they are like those £10,000-a-spoon desserts in a Michelin-starred restaurant—using RSC sparingly when encountering certain problems feels incredibly comfortable and elegant. However, we are not only discussing applications with complex data models—where Server Actions might not be much better—but also solutions for pure client-side applications.

Since the backend needs to remain “pure” but an intermediate layer is required, Codegen comes to mind. Conveniently, drizzle-orm even allows us to generate Zod schemas, killing two birds with one stone! Here, “codegen” means generating SWR hooks based on a schema. It’s actually a good idea, but it doesn’t solve the mutation association problem—the issue of which data change operation triggers which Key mutation.

Finally, we are left with options like GraphQL. When I mention GraphQL, it’s currently in an awkward spot: if the app is already complex, it means a refactoring cost as high as tRPC. If the app itself is toy-level, why bother? To solve a small problem of over-engineering, we introduce a massive over-engineered design… that’s not good.

Slightly Lost#

Consequently, after a long period of thought experiments, I haven’t actually made any sufficient improvements to the original architecture. However, over the past two months, I have tried:

  • Service Locator Architecture: Categorizing SWR hooks into “service” objects, with all services provided via React Context.
  • Boilerplate reduction utilities: Quickly wrapping IPC functions or fetch /api/ calls.
  • Key Shapes: In my previous article, I adopted a Key shape based on { endpoint: ..., /* other params */ }. But I’ve found that the array-based Keys encouraged by TanStack Query might even be better. I once assumed I had the patience to meticulously control the mutation of each individual key. In the end, I realized that once you have a mess of endpoints like “all models,” “user-added models,” “user-pinned models,” “true all models,” and “all models in the list,” you just end up invalidating everything in a single blanket mutation anyway. I am currently experimenting with and feeling the “correctness” of ['models', 'pinned'] over { endpoint: 'pinnedModels' }.

For now, although the SWR layer code looks massive on the frontend, its operation remains stable and reliable. When adding a new feature, you only need to mindlessly copy some templates. In the future, I may release a V2 of Safe & Consistent SWR / Query, including all updated best practices. For now, I still believe this is the most reliable and effective data fetching solution.

After Safe & Consistent SWR
https://eflx.top/blog/after-safe-consistent-swr
Author EFL
Published at April 17, 2026