fate
A Relay-inspired data client for React with normalized caching, view composition, and type-safe fetching.
Repository Health
Technical Analysis
fate is a modern data client for React that brings Relay and GraphQL’s best ideas — co-located view composition, a normalized cache, data masking, and cursor-based pagination — to plain TypeScript without requiring a GraphQL server. Components declare their data needs through composable “views,” fate merges them into a single request per screen, and React Suspense plus Actions handle loading and error states automatically.
It ships adapters for tRPC, GraphQL, and raw HTTP transports, plus first-class integrations for Prisma, Drizzle, Vite, and Cloudflare Workers, making it usable on teams that already have a type-safe RPC layer but want Relay-grade data-fetching ergonomics on the client.
What You Get
- A
view()API for declaring per-component data selections that compose upward into a single request per screen - A normalized, entity-keyed cache with automatic re-rendering scoped to only the fields a component actually selected
- Data masking that prevents components from reading fields they didn’t request, avoiding hidden coupling
- Built-in cursor-based pagination and list helpers for infinite scroll and load-more UIs
- Declarative optimistic updates for mutations with automatic rollback on failure
- Live view support that streams updates into the cache over Server-Sent Events
- Pluggable transports — tRPC, GraphQL, or raw HTTP — plus server helpers for Prisma and Drizzle
Common Use Cases
- Adding Relay-style fragment composition and normalized caching to a React app backed by tRPC instead of GraphQL
- Replacing ad-hoc
useEffectdata fetching with a single per-screen request that Suspense manages - Building infinite-scroll or paginated list UIs with connection-style cursors out of the box
- Adding optimistic mutation updates without hand-rolling cache patching and rollback logic
- Streaming live updates (e.g. presence, notifications, chat) into an already-normalized client cache
Under The Hood
Architecture fate splits its client into focused modules rather than one monolith: client.ts orchestrates request/selection/mutation lifecycles, store.ts and cache.ts implement the normalized entity store and garbage collection, mask.ts enforces field-level data masking, selection.ts/request-descriptor.ts compile view trees into selection plans, and hydration.ts handles server-to-client state transfer. Transport concerns are isolated behind transport.ts with dedicated adapters (httpTransport.ts, graphqlTransport.ts, tRPC via transport.ts), and server-side helpers for Prisma/Drizzle live under src/server/. This separation lets the same core client logic run against three different backend styles without branching.
Tech Stack Written entirely in TypeScript (92% of the repo) as a pnpm/Vite-Plus monorepo with six packages (fate, react-fate, vue-fate, void-fate, cf-fate, create-fate). Runtime dependencies are minimal — just zod for schema validation and superjson for rich serialization — with @trpc/client, drizzle-orm, graphql, and vite treated as optional peer dependencies so consumers only pull in what they use. Builds go through a custom vp pack pipeline targeting Node 24 with .mjs/.d.mts output.
Code Quality The core fate package alone has 27 Vitest test files covering the cache, store, selection compiler, transports (HTTP, GraphQL, tRPC), hydration, view resolution, and garbage collection, plus dedicated server-side tests for Drizzle/Prisma adapters. The project enforces its own ESLint/Oxlint configs (@nkzw/eslint-plugin, @nkzw/oxlint-config) and strict TypeScript throughout. The core client module (client.ts, ~3,400 lines) is large and carries meaningful internal complexity, though it’s offset by the surrounding modules being small and single-purpose.
API Design The public surface exported from index.ts is deliberately narrow — view, useView/useRequest (via react-fate), createClient, and a handful of transport factories — with everything else kept internal. Getting started requires defining a view and calling useRequest/useView, mirroring Relay’s fragment ergonomics but without a GraphQL compiler step. The tradeoff is a genuinely new mental model (views, normalized refs, data masking) that developers coming from plain fetch/React Query patterns need to learn, even though the API itself is small and consistent.