From 57e7b24fccf833ff3d1f54de31ba15784e6a4b2a Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 13 May 2026 14:31:40 -0400 Subject: [PATCH 1/2] commit for sonar check --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 4e7120b14..c05851acc 100644 --- a/readme.md +++ b/readme.md @@ -366,3 +366,4 @@ Have feedback? Leave a comment in [CellixJS discussions on GitHub](https://githu [![sharethrift contributors](https://contrib.rocks/image?repo=cellixjs/cellixjs)](https://github.com/cellixjs/cellixjs/graphs/contributors) [⬆ Back to Top](#table-of-contents) + From ef845a563d2c9083b46186246a16e9b3506b9aac Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 13 May 2026 14:56:02 -0400 Subject: [PATCH 2/2] Added skills files, minus some overlap, that were present in sharethrift but not here --- .agents/skills/apollo-client/SKILL.md | 168 + .../apollo-client/references/caching.md | 557 +++ .../references/error-handling.md | 337 ++ .../apollo-client/references/fragments.md | 773 ++++ .../references/integration-client.md | 337 ++ .../references/integration-nextjs.md | 317 ++ .../references/integration-react-router.md | 253 ++ .../references/integration-tanstack-start.md | 365 ++ .../apollo-client/references/mutations.md | 540 +++ .../apollo-client/references/queries.md | 421 ++ .../references/state-management.md | 412 ++ .../references/suspense-hooks.md | 764 ++++ .../references/troubleshooting.md | 491 +++ .../references/typescript-codegen.md | 130 + .agents/skills/apollo-mcp-server/SKILL.md | 306 ++ .../references/configuration.md | 485 +++ .../apollo-mcp-server/references/tools.md | 302 ++ .../references/troubleshooting.md | 309 ++ .agents/skills/apollo-server/SKILL.md | 294 ++ .../references/context-and-auth.md | 477 +++ .../apollo-server/references/data-sources.md | 403 ++ .../references/error-handling.md | 447 +++ .../apollo-server/references/plugins.md | 438 +++ .../apollo-server/references/resolvers.md | 321 ++ .../references/troubleshooting.md | 494 +++ .agents/skills/graphql-operations/SKILL.md | 244 ++ .../references/fragments.md | 536 +++ .../references/mutations.md | 435 +++ .../graphql-operations/references/queries.md | 504 +++ .../graphql-operations/references/tooling.md | 404 ++ .../references/variables.md | 440 +++ .agents/skills/graphql-schema/SKILL.md | 172 + .../graphql-schema/references/errors.md | 388 ++ .../graphql-schema/references/naming.md | 400 ++ .../graphql-schema/references/pagination.md | 396 ++ .../graphql-schema/references/security.md | 484 +++ .../skills/graphql-schema/references/types.md | 445 +++ .../skills/typescript-advanced-types/SKILL.md | 717 ++++ .../vercel-react-best-practices/AGENTS.md | 3373 +++++++++++++++++ .../vercel-react-best-practices/README.md | 123 + .../vercel-react-best-practices/SKILL.md | 143 + .../rules/_sections.md | 46 + .../rules/_template.md | 28 + .../rules/advanced-event-handler-refs.md | 55 + .../rules/advanced-init-once.md | 42 + .../rules/advanced-use-latest.md | 39 + .../rules/async-api-routes.md | 38 + .../rules/async-defer-await.md | 80 + .../rules/async-dependencies.md | 51 + .../rules/async-parallel.md | 28 + .../rules/async-suspense-boundaries.md | 99 + .../rules/bundle-barrel-imports.md | 59 + .../rules/bundle-conditional.md | 31 + .../rules/bundle-defer-third-party.md | 49 + .../rules/bundle-dynamic-imports.md | 35 + .../rules/bundle-preload.md | 50 + .../rules/client-event-listeners.md | 74 + .../rules/client-localstorage-schema.md | 71 + .../rules/client-passive-event-listeners.md | 48 + .../rules/client-swr-dedup.md | 56 + .../rules/js-batch-dom-css.md | 107 + .../rules/js-cache-function-results.md | 80 + .../rules/js-cache-property-access.md | 28 + .../rules/js-cache-storage.md | 70 + .../rules/js-combine-iterations.md | 32 + .../rules/js-early-exit.md | 50 + .../rules/js-flatmap-filter.md | 60 + .../rules/js-hoist-regexp.md | 45 + .../rules/js-index-maps.md | 37 + .../rules/js-length-check-first.md | 49 + .../rules/js-min-max-loop.md | 82 + .../rules/js-set-map-lookups.md | 24 + .../rules/js-tosorted-immutable.md | 57 + .../rules/rendering-activity.md | 26 + .../rules/rendering-animate-svg-wrapper.md | 47 + .../rules/rendering-conditional-render.md | 40 + .../rules/rendering-content-visibility.md | 38 + .../rules/rendering-hoist-jsx.md | 46 + .../rules/rendering-hydration-no-flicker.md | 82 + .../rendering-hydration-suppress-warning.md | 30 + .../rules/rendering-resource-hints.md | 85 + .../rules/rendering-script-defer-async.md | 68 + .../rules/rendering-svg-precision.md | 28 + .../rules/rendering-usetransition-loading.md | 75 + .../rules/rerender-defer-reads.md | 39 + .../rules/rerender-dependencies.md | 45 + .../rules/rerender-derived-state-no-effect.md | 40 + .../rules/rerender-derived-state.md | 29 + .../rules/rerender-functional-setstate.md | 74 + .../rules/rerender-lazy-state-init.md | 58 + .../rules/rerender-memo-with-default-value.md | 38 + .../rules/rerender-memo.md | 44 + .../rules/rerender-move-effect-to-event.md | 45 + .../rules/rerender-no-inline-components.md | 82 + .../rerender-simple-expression-in-memo.md | 35 + .../rules/rerender-split-combined-hooks.md | 64 + .../rules/rerender-transitions.md | 40 + .../rules/rerender-use-deferred-value.md | 59 + .../rerender-use-ref-transient-values.md | 73 + .../rules/server-after-nonblocking.md | 73 + .../rules/server-auth-actions.md | 96 + .../rules/server-cache-lru.md | 41 + .../rules/server-cache-react.md | 76 + .../rules/server-dedup-props.md | 65 + .../rules/server-hoist-static-io.md | 142 + .../rules/server-parallel-fetching.md | 83 + .../rules/server-serialization.md | 38 + package.json | 9 +- .../server-oauth2-mock-seedwork/package.json | 4 +- ...ogged-in-user-community.container.test.tsx | 21 +- .../logged-in-user-root.container.test.tsx | 31 +- pnpm-lock.yaml | 566 ++- skills-lock.json | 35 + 113 files changed, 23538 insertions(+), 117 deletions(-) create mode 100644 .agents/skills/apollo-client/SKILL.md create mode 100644 .agents/skills/apollo-client/references/caching.md create mode 100644 .agents/skills/apollo-client/references/error-handling.md create mode 100644 .agents/skills/apollo-client/references/fragments.md create mode 100644 .agents/skills/apollo-client/references/integration-client.md create mode 100644 .agents/skills/apollo-client/references/integration-nextjs.md create mode 100644 .agents/skills/apollo-client/references/integration-react-router.md create mode 100644 .agents/skills/apollo-client/references/integration-tanstack-start.md create mode 100644 .agents/skills/apollo-client/references/mutations.md create mode 100644 .agents/skills/apollo-client/references/queries.md create mode 100644 .agents/skills/apollo-client/references/state-management.md create mode 100644 .agents/skills/apollo-client/references/suspense-hooks.md create mode 100644 .agents/skills/apollo-client/references/troubleshooting.md create mode 100644 .agents/skills/apollo-client/references/typescript-codegen.md create mode 100644 .agents/skills/apollo-mcp-server/SKILL.md create mode 100644 .agents/skills/apollo-mcp-server/references/configuration.md create mode 100644 .agents/skills/apollo-mcp-server/references/tools.md create mode 100644 .agents/skills/apollo-mcp-server/references/troubleshooting.md create mode 100644 .agents/skills/apollo-server/SKILL.md create mode 100644 .agents/skills/apollo-server/references/context-and-auth.md create mode 100644 .agents/skills/apollo-server/references/data-sources.md create mode 100644 .agents/skills/apollo-server/references/error-handling.md create mode 100644 .agents/skills/apollo-server/references/plugins.md create mode 100644 .agents/skills/apollo-server/references/resolvers.md create mode 100644 .agents/skills/apollo-server/references/troubleshooting.md create mode 100644 .agents/skills/graphql-operations/SKILL.md create mode 100644 .agents/skills/graphql-operations/references/fragments.md create mode 100644 .agents/skills/graphql-operations/references/mutations.md create mode 100644 .agents/skills/graphql-operations/references/queries.md create mode 100644 .agents/skills/graphql-operations/references/tooling.md create mode 100644 .agents/skills/graphql-operations/references/variables.md create mode 100644 .agents/skills/graphql-schema/SKILL.md create mode 100644 .agents/skills/graphql-schema/references/errors.md create mode 100644 .agents/skills/graphql-schema/references/naming.md create mode 100644 .agents/skills/graphql-schema/references/pagination.md create mode 100644 .agents/skills/graphql-schema/references/security.md create mode 100644 .agents/skills/graphql-schema/references/types.md create mode 100644 .agents/skills/typescript-advanced-types/SKILL.md create mode 100644 .agents/skills/vercel-react-best-practices/AGENTS.md create mode 100644 .agents/skills/vercel-react-best-practices/README.md create mode 100644 .agents/skills/vercel-react-best-practices/SKILL.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/_sections.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/_template.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-init-once.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-api-routes.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-defer-await.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-dependencies.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-parallel.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-conditional.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-preload.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-event-listeners.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-storage.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-early-exit.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-index-maps.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-length-check-first.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-activity.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-memo.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-transitions.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-auth-actions.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-cache-lru.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-cache-react.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-dedup-props.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-serialization.md diff --git a/.agents/skills/apollo-client/SKILL.md b/.agents/skills/apollo-client/SKILL.md new file mode 100644 index 000000000..cdc564f52 --- /dev/null +++ b/.agents/skills/apollo-client/SKILL.md @@ -0,0 +1,168 @@ +--- +name: apollo-client +description: > + Guide for building React applications with Apollo Client 4.x. Use this skill when: + (1) setting up Apollo Client in a React project, + (2) writing GraphQL queries or mutations with hooks, + (3) configuring caching or cache policies, + (4) managing local state with reactive variables, + (5) troubleshooting Apollo Client errors or performance issues. +license: MIT +compatibility: React 18+, React 19 (Suspense/RSC). Works with Next.js, Vite, CRA, and other React frameworks. +metadata: + author: apollographql + version: "1.0.0" +allowed-tools: Bash(npm:*) Bash(npx:*) Bash(node:*) Read Write Edit Glob Grep +--- + +# Apollo Client 4.x Guide + +Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. Version 4.x brings improved caching, better TypeScript support, and React 19 compatibility. + +## Integration Guides + +Choose the integration guide that matches your application setup: + +- **[Client-Side Apps](references/integration-client.md)** - For client-side React applications without SSR (Vite, Create React App, etc.) +- **[Next.js App Router](references/integration-nextjs.md)** - For Next.js applications using the App Router with React Server Components +- **[React Router Framework Mode](references/integration-react-router.md)** - For React Router 7 applications with streaming SSR +- **[TanStack Start](references/integration-tanstack-start.md)** - For TanStack Start applications with modern routing + +Each guide includes installation steps, configuration, and framework-specific patterns optimized for that environment. + +## Quick Reference + +### Basic Query + +```tsx +import { gql } from "@apollo/client"; +import { useQuery } from "@apollo/client/react"; + +const GET_USER = gql` + query GetUser($id: ID!) { + user(id: $id) { + id + name + } + } +`; + +function UserProfile({ userId }: { userId: string }) { + const { loading, error, data, dataState } = useQuery(GET_USER, { + variables: { id: userId }, + }); + + if (loading) return

Loading...

; + if (error) return

Error: {error.message}

; + + // TypeScript: dataState === "ready" provides better type narrowing than just checking data + return
{data.user.name}
; +} +``` + +### Basic Mutation + +```tsx +import { gql } from "@apollo/client"; +import { useMutation } from "@apollo/client/react"; + +const CREATE_USER = gql` + mutation CreateUser($input: CreateUserInput!) { + createUser(input: $input) { + id + name + } + } +`; + +function CreateUserForm() { + const [createUser, { loading, error }] = useMutation(CREATE_USER); + + const handleSubmit = async (name: string) => { + await createUser({ variables: { input: { name } } }); + }; + + return ; +} +``` + +### Suspense Query + +```tsx +import { Suspense } from "react"; +import { useSuspenseQuery } from "@apollo/client/react"; + +function UserProfile({ userId }: { userId: string }) { + const { data } = useSuspenseQuery(GET_USER, { + variables: { id: userId }, + }); + + return
{data.user.name}
; +} + +function App() { + return ( + Loading user...

}> + +
+ ); +} +``` + +## Reference Files + +Detailed documentation for specific topics: + +- [TypeScript Code Generation](references/typescript-codegen.md) - GraphQL Code Generator setup for type-safe operations +- [Queries](references/queries.md) - useQuery, useLazyQuery, polling, refetching +- [Suspense Hooks](references/suspense-hooks.md) - useSuspenseQuery, useBackgroundQuery, useReadQuery, useLoadableQuery +- [Mutations](references/mutations.md) - useMutation, optimistic UI, cache updates +- [Fragments](references/fragments.md) - Fragment colocation, useFragment, useSuspenseFragment, data masking +- [Caching](references/caching.md) - InMemoryCache, typePolicies, cache manipulation +- [State Management](references/state-management.md) - Reactive variables, local state +- [Error Handling](references/error-handling.md) - Error policies, error links, retries +- [Troubleshooting](references/troubleshooting.md) - Common issues and solutions + +## Key Rules + +### Query Best Practices + +- **Each page should generally only have one query, composed from colocated fragments.** Use `useFragment` or `useSuspenseFragment` in all non-page-components. Use `@defer` to allow slow fields below the fold to stream in later and avoid blocking the page load. +- **Fragments are for colocation, not reuse.** Each fragment should describe exactly the data needs of a specific component, not be shared across components for common fields. See [Fragments reference](references/fragments.md) for details on fragment colocation and data masking. +- Always handle `loading` and `error` states in UI when using non-suspenseful hooks (`useQuery`, `useLazyQuery`). When using Suspense hooks (`useSuspenseQuery`, `useBackgroundQuery`), React handles this through `` boundaries and error boundaries. +- Use `fetchPolicy` to control cache behavior per query +- Use the TypeScript type server to look up documentation for functions and options (Apollo Client has extensive docblocks) + +### Mutation Best Practices + +- **If the schema permits, mutation return values should return everything necessary to update the cache.** Neither manual updates nor refetching should be necessary. +- If the mutation response is insufficient, carefully weigh manual cache manipulation vs refetching. Manual updates risk missing server logic. Consider optimistic updates with a granular refetch if needed. +- Handle errors gracefully in the UI +- Use `refetchQueries` sparingly (prefer letting the cache update automatically) + +### Caching Best Practices + +- Configure `keyFields` for types without `id` field +- Disable normalization by setting `keyFields: false` for types that don't include an identifier and are meant to group related fields under the parent +- Use `typePolicies` for pagination and computed fields +- Understand cache normalization to debug issues +- **Enable [data masking](references/fragments.md#data-masking) for all new applications** - it prevents components from accessing fragment data they don't own, enforcing proper data boundaries and preventing over-rendering + +### Performance + +- Avoid over-fetching with proper field selection +- Configure appropriate `fetchPolicy` per use case +- Use `@defer` for incremental delivery of deferred query parts, and `@stream` for streaming list fields (`@stream` available in Apollo Client 4.1+) +- **Prefer Suspense hooks (`useSuspenseQuery`, `useBackgroundQuery`) in modern applications** for better loading state handling and code simplicity + +## Ground Rules + +- ALWAYS use Apollo Client 4.x patterns (not v3 or earlier) +- ALWAYS wrap your app with `ApolloProvider` +- ALWAYS handle loading and error states when using non-suspenseful hooks +- PREFER Suspense hooks (`useSuspenseQuery`, `useBackgroundQuery`) in modern applications for better DX +- NEVER store Apollo Client in React state (use module-level or context) +- PREFER `cache-first` for read-heavy data, `network-only` for real-time data +- USE TypeScript for better type safety with GraphQL +- IMPLEMENT proper cache updates instead of refetching entire queries +- ADVISE the human steering you to look into Apollo DevTools when collaboratively debugging Apollo Client issues diff --git a/.agents/skills/apollo-client/references/caching.md b/.agents/skills/apollo-client/references/caching.md new file mode 100644 index 000000000..7009edd18 --- /dev/null +++ b/.agents/skills/apollo-client/references/caching.md @@ -0,0 +1,557 @@ +# Caching Reference + +## Table of Contents + +- [InMemoryCache Setup](#inmemorycache-setup) +- [Cache Normalization](#cache-normalization) +- [Type Policies](#type-policies) +- [Field Policies](#field-policies) +- [Pagination](#pagination) +- [Cache Manipulation](#cache-manipulation) +- [Garbage Collection](#garbage-collection) + +## InMemoryCache Setup + +### Basic Configuration + +```typescript +import { InMemoryCache } from "@apollo/client"; + +const cache = new InMemoryCache({ + // Custom type policies + typePolicies: { + Query: { + fields: { + // Query-level field policies + }, + }, + User: { + keyFields: ["id"], + fields: { + // User-level field policies + }, + }, + }, + + // Custom type name handling (rare) + possibleTypes: { + Character: ["Human", "Droid"], + Node: ["User", "Post", "Comment"], + }, +}); +``` + +### Constructor Options + +```typescript +new InMemoryCache({ + // Define how types are identified in cache + typePolicies: { + /* ... */ + }, + + // Interface/union type mappings between supertypes and their subtypes + possibleTypes: { + /* ... */ + }, + + // Custom function to generate cache IDs (rare) + dataIdFromObject: (object) => { + if (object.__typename === "Book") { + return `Book:${object.isbn}`; + } + return defaultDataIdFromObject(object); + }, +}); +``` + +## Cache Normalization + +Apollo Client normalizes data by splitting query results into individual objects and storing them by unique identifier. + +### How Normalization Works + +```graphql +# Query +query GetPost { + post(id: "1") { + id + title + author { + id + name + } + } +} +``` + +```typescript +// Normalized cache structure +{ + 'Post:1': { + __typename: 'Post', + id: '1', + title: 'Hello World', + author: { __ref: 'User:42' } + }, + 'User:42': { + __typename: 'User', + id: '42', + name: 'John' + }, + ROOT_QUERY: { + 'post({"id":"1"})': { __ref: 'Post:1' } + } +} +``` + +### Benefits of Normalization + +1. **Automatic updates**: When `User:42` is updated anywhere, all components showing that user update +2. **Deduplication**: Same objects aren't stored multiple times +3. **Efficient updates**: Only changed objects trigger re-renders + +## Type Policies + +### keyFields + +Customize how objects are identified in the cache. + +```typescript +const cache = new InMemoryCache({ + typePolicies: { + // Use ISBN instead of id for books + Book: { + keyFields: ["isbn"], + }, + + // Composite key + UserSession: { + keyFields: ["userId", "deviceId"], + }, + + // Nested key + Review: { + keyFields: ["book", ["isbn"], "reviewer", ["id"]], + }, + + // No key fields (singleton, only one object in cache per type) + AppSettings: { + keyFields: [], + }, + + // Disable normalization (objects of this type will be stored with their + // parent entity. The same object might end up multiple times in the cache + // and run out of sync. Use with caution, only if this object really relates + // to a property of their parent entity and cannot exist on its own.) + Address: { + keyFields: false, + }, + }, +}); +``` + +### merge Functions + +Control how incoming data merges with existing data. + +```typescript +const cache = new InMemoryCache({ + typePolicies: { + User: { + fields: { + // Deep merge profile object + profile: { + merge: true, // Shorthand for deep merge + }, + + // Custom merge logic + notifications: { + merge(existing = [], incoming, { mergeObjects }) { + // Prepend new notifications + return [...incoming, ...existing]; + }, + }, + }, + }, + }, +}); +``` + +## Field Policies + +### read Function + +Transform cached data when reading. + +```typescript +const cache = new InMemoryCache({ + typePolicies: { + User: { + fields: { + // Computed field + fullName: { + read(_, { readField }) { + const firstName = readField("firstName"); + const lastName = readField("lastName"); + return `${firstName} ${lastName}`; + }, + }, + + // Transform existing field + birthDate: { + read(existing) { + return existing ? new Date(existing) : null; + }, + }, + + // Default value + role: { + read(existing = "USER") { + return existing; + }, + }, + }, + }, + }, +}); +``` + +### merge Function + +Control how incoming data is stored. + +```typescript +const cache = new InMemoryCache({ + typePolicies: { + User: { + fields: { + // Accumulate items instead of replacing + friends: { + merge(existing = [], incoming) { + return [...existing, ...incoming]; + }, + }, + + // Merge objects deeply + settings: { + merge(existing, incoming, { mergeObjects }) { + return mergeObjects(existing, incoming); + }, + }, + }, + }, + + Query: { + fields: { + // Merge paginated results + posts: { + keyArgs: ["category"], // Only category affects cache key + merge(existing = { items: [] }, incoming) { + return { + ...incoming, + items: [...existing.items, ...incoming.items], + }; + }, + }, + }, + }, + }, +}); +``` + +### keyArgs + +Control which arguments affect cache storage. + +```typescript +const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + // Different cache entry per userId only + // (limit, offset don't create new entries) + userPosts: { + keyArgs: ["userId"], + }, + + // No arguments affect cache key + // (useful for pagination) + feed: { + keyArgs: false, + }, + + // Nested argument + search: { + keyArgs: ["filter", ["category", "status"]], + }, + }, + }, + }, +}); +``` + +## Pagination + +### Offset-Based Pagination + +```typescript +import { offsetLimitPagination } from "@apollo/client/utilities"; + +const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + posts: offsetLimitPagination(), + + // With key arguments + userPosts: offsetLimitPagination(["userId"]), + }, + }, + }, +}); +``` + +### Cursor-Based Pagination (Relay Style) + +```typescript +import { relayStylePagination } from "@apollo/client/utilities"; + +const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + posts: relayStylePagination(), + + // With key arguments + userPosts: relayStylePagination(["userId"]), + }, + }, + }, +}); +``` + +### Custom Pagination + +```typescript +const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + feed: { + keyArgs: false, + + merge(existing, incoming, { args }) { + const merged = existing ? existing.slice(0) : []; + const offset = args?.offset ?? 0; + + for (let i = 0; i < incoming.length; i++) { + merged[offset + i] = incoming[i]; + } + + return merged; + }, + + read(existing, { args }) { + const offset = args?.offset ?? 0; + const limit = args?.limit ?? existing?.length ?? 0; + return existing?.slice(offset, offset + limit); + }, + }, + }, + }, + }, +}); +``` + +### fetchMore for Pagination + +```tsx +function PostList() { + const { data, fetchMore, loading } = useQuery(GET_POSTS, { + variables: { offset: 0, limit: 10 }, + }); + + const loadMore = () => { + fetchMore({ + variables: { + offset: data.posts.length, + }, + // With proper type policies, no updateQuery needed + }); + }; + + return ( +
+ {data?.posts.map((post) => ( + + ))} + +
+ ); +} +``` + +## Cache Manipulation + +### cache.readQuery + +```typescript +// Read data from cache +const data = cache.readQuery({ + query: GET_TODOS, +}); + +// With variables +const userData = cache.readQuery({ + query: GET_USER, + variables: { id: "1" }, +}); +``` + +### cache.writeQuery + +```typescript +// Write data to cache +cache.writeQuery({ + query: GET_TODOS, + data: { + todos: [{ __typename: "Todo", id: "1", text: "Buy milk", completed: false }], + }, +}); + +// With variables +cache.writeQuery({ + query: GET_USER, + variables: { id: "1" }, + data: { + user: { __typename: "User", id: "1", name: "John" }, + }, +}); +``` + +### cache.readFragment / cache.writeFragment + +```typescript +// Read a specific object - use cache.identify for safety +const user = cache.readFragment({ + id: cache.identify({ __typename: "User", id: "1" }), + fragment: gql` + fragment UserFragment on User { + id + name + email + } + `, +}); + +// Apollo Client 4.1+: Use 'from' parameter (recommended) +const user = cache.readFragment({ + from: { __typename: "User", id: "1" }, + fragment: gql` + fragment UserFragment on User { + id + name + email + } + `, +}); + +// Update a specific object +cache.writeFragment({ + id: cache.identify({ __typename: "User", id: "1" }), + fragment: gql` + fragment UpdateUser on User { + name + } + `, + data: { + name: "Jane", + }, +}); + +// Apollo Client 4.1+: Use 'from' parameter (recommended) +cache.writeFragment({ + from: { __typename: "User", id: "1" }, + fragment: gql` + fragment UpdateUser on User { + name + } + `, + data: { + name: "Jane", + }, +}); +``` + +### cache.modify + +```typescript +// Modify fields directly +cache.modify({ + id: cache.identify(user), + fields: { + // Set new value + name: () => "New Name", + + // Transform existing value + postCount: (existing) => existing + 1, + + // Delete field + temporaryField: (_, { DELETE }) => DELETE, + + // Add to array + friends: (existing, { toReference }) => [...existing, toReference({ __typename: "User", id: "2" })], + }, +}); +``` + +### cache.evict + +```typescript +// Remove object from cache +cache.evict({ id: "User:1" }); + +// Remove specific field +cache.evict({ id: "User:1", fieldName: "friends" }); + +// Remove with broadcast (trigger re-renders) +cache.evict({ id: "User:1", broadcast: true }); +``` + +## Garbage Collection + +### Manual Garbage Collection + +```typescript +// After evicting objects, clean up dangling references +cache.evict({ id: "User:1" }); +cache.gc(); +``` + +### Retaining Objects + +```typescript +// Prevent objects from being garbage collected +const release = cache.retain("User:1"); + +// Later, allow GC +release(); +cache.gc(); +``` + +### Inspecting Cache + +```typescript +// Get all cached data +const cacheContents = cache.extract(); + +// Restore cache state +cache.restore(previousCacheContents); + +// Get identified object cache key +const userId = cache.identify({ __typename: "User", id: "1" }); +// Returns: 'User:1' +``` diff --git a/.agents/skills/apollo-client/references/error-handling.md b/.agents/skills/apollo-client/references/error-handling.md new file mode 100644 index 000000000..8f95ebe01 --- /dev/null +++ b/.agents/skills/apollo-client/references/error-handling.md @@ -0,0 +1,337 @@ +# Error Handling Reference (Apollo Client 4.x) + +Note that Apollo Client 4.x handles errors differently than Apollo Client 3.x. +This reference documents the updated error handling mechanisms, error types, and best practices for managing errors in your Apollo Client applications. +For older Apollo Client 3.x error handling documentation, see [Apollo Client 3.x Error Handling](https://www.apollographql.com/docs/react/v3/data/error-handling). + +## Table of Contents + +- [Understanding Errors](#understanding-errors) +- [Error Types](#error-types) +- [Identifying Error Types](#identifying-error-types) +- [GraphQL Error Policies](#graphql-error-policies) +- [Error Links](#error-links) +- [Retry Logic](#retry-logic) +- [Error Boundaries](#error-boundaries) + +## Understanding Errors + +Errors in Apollo Client fall into two main categories: **GraphQL errors** and **network errors**. Each category has specific error classes that provide detailed information about what went wrong. + +### GraphQL Errors + +GraphQL errors are related to server-side execution of a GraphQL operation: + +- **Syntax errors** (e.g., malformed query) +- **Validation errors** (e.g., query includes a non-existent schema field) +- **Resolver errors** (e.g., error while populating a query field) + +If a syntax or validation error occurs, the server doesn't execute the operation. If resolver errors occur, the server can still return partial data. + +Example server response with GraphQL error: + +```json +{ + "errors": [ + { + "message": "Cannot query field \"nonexistentField\" on type \"Query\".", + "locations": [{ "line": 2, "column": 3 }], + "extensions": { + "code": "GRAPHQL_VALIDATION_FAILED" + } + } + ], + "data": null +} +``` + +In Apollo Client 4.x, GraphQL errors are represented by the [`CombinedGraphQLErrors`](https://apollographql.com/docs/react/api/errors/CombinedGraphQLErrors) error type. + +### Network Errors + +Network errors occur when attempting to communicate with your GraphQL server: + +- `4xx` or `5xx` HTTP response status codes +- Network unavailability +- JSON parsing failures +- Custom errors from Apollo Link request handlers + +Network errors might be represented by special error types, but if an api such as the `fetch` API throws a native error (e.g., `TypeError`), Apollo Client will pass it through as-is. +Thrown values that don't fulfill the standard `ErrorLike` interface are wrapped in the [`UnconventionalError`](https://apollographql.com/docs/react/api/errors/UnconventionalError) class, which fulfills the `ErrorLike` interface. As such, you can expect any error returned by Apollo Client to fulfill the `ErrorLike` interface. + +```ts +export interface ErrorLike { + message: string; + name: string; + stack?: string; +} +``` + +## Error Types + +Apollo Client 4.x provides specific error classes for different error scenarios: + +### CombinedGraphQLErrors + +Represents GraphQL errors returned by the server. Most common error type in applications. + +```tsx +import { CombinedGraphQLErrors } from "@apollo/client/errors"; + +function UserProfile({ userId }: { userId: string }) { + const { data, error } = useQuery(GET_USER, { + variables: { id: userId }, + }); + + // no need to check for nullishness of error, CombinedGraphQLErrors.is handles that + if (CombinedGraphQLErrors.is(error)) { + // Handle GraphQL errors + return ( +
+ {error.graphQLErrors.map((err, i) => ( +

GraphQL Error: {err.message}

+ ))} +
+ ); + } + + return data ? : null; +} +``` + +### CombinedProtocolErrors + +Represents fatal transport-level errors during multipart HTTP subscription execution. + +### ServerError + +Occurs when the server responds with a non-200 HTTP status code. + +```tsx +import { ServerError } from "@apollo/client/errors"; + +if (ServerError.is(error)) { + console.error("Server error:", error.statusCode, error.result); +} +``` + +### ServerParseError + +Occurs when the server response cannot be parsed as valid JSON. + +```tsx +import { ServerParseError } from "@apollo/client/errors"; + +if (ServerParseError.is(error)) { + console.error("Invalid JSON response:", error.bodyText); +} +``` + +### LocalStateError + +Represents errors in local state configuration or execution. + +### UnconventionalError + +Wraps non-standard errors (e.g., thrown symbols or objects) to ensure consistent error handling. + +## Identifying Error Types + +Every Apollo Client error class provides a static `is` method that reliably determines whether an error is of that specific type. This is more robust than `instanceof` because it avoids false positives/negatives. + +```ts +import { + CombinedGraphQLErrors, + CombinedProtocolErrors, + LocalStateError, + ServerError, + ServerParseError, + UnconventionalError, + ErrorLike, +} from "@apollo/client/errors"; + +// Anything returned in the `error` field of Apollo Client hooks or methods is of type `ErrorLike` or `undefined`. +function handleError(error?: ErrorLike) { + if (CombinedGraphQLErrors.is(error)) { + // Handle GraphQL errors + console.error("GraphQL errors:", error.graphQLErrors); + } else if (CombinedProtocolErrors.is(error)) { + // Handle multipart subscription protocol errors + } else if (LocalStateError.is(error)) { + // Handle errors thrown by the LocalState class + } else if (ServerError.is(error)) { + // Handle server HTTP errors + console.error("Server error:", error.statusCode); + } else if (ServerParseError.is(error)) { + // Handle JSON parse errors + } else if (UnconventionalError.is(error)) { + // Handle errors thrown by irregular types + } else if (error) { + // Handle other errors + } +} +``` + +## GraphQL Error Policies + +If a GraphQL operation produces errors, the server's response might still include partial data: + +```json +{ + "data": { + "getInt": 12, + "getString": null + }, + "errors": [ + { + "message": "Failed to get string!" + } + ] +} +``` + +By default, Apollo Client throws away partial data and populates the `error` field. You can use partial results by defining an **error policy**: + +| Policy | Description | +| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `none` | (Default) If the response includes errors, they are returned in `error` and response `data` is set to `undefined` even if the server returns `data`. | +| `ignore` | Errors are ignored (`error` is not populated), and any returned `data` is cached and rendered as if no errors occurred. `data` may be `undefined` if a network error occurs. | +| `all` | Both `data` and `error` are populated and any returned `data` is cached, enabling you to render both partial results and error information. | + +### Setting an Error Policy + +```tsx +const MY_QUERY = gql` + query WillFail { + badField # This field's resolver produces an error + goodField # This field is populated successfully + } +`; + +function ShowingSomeErrors() { + const { loading, error, data } = useQuery(MY_QUERY, { errorPolicy: "all" }); + + if (loading) return loading...; + + return ( +
+

Good: {data?.goodField}

+ {error &&
Bad: {error.message}
} +
+ ); +} +``` + +### Avoid setting a Global Error Policy + +While it is possible to set a global error policy using `defaultOptions`, in practice this is discouraged as it can lead to unexpected behavior and type safety issues. The return types of the TypeScript hooks may change depending on the `errorPolicy` passed into the hook, and this can conceptually not take global `defaultOptions` error policies into account. As such, it is best to set the `errorPolicy` per operation as needed. + +## Error Links + +The `ErrorLink` can be used to e.g. log error globally or perform specific side effects based on errors happening. + +An `ErrorLink` can't be used to swallow errors fully, but it can be used to retry an operation after handling an error, in which case the error wouldn't propagate. Otherwise, the most common use for `ErrorLink` is logging. + +```ts +import { ErrorLink } from "@apollo/client/link/error"; + +const errorLink = new ErrorLink(({ error, operation, forward }) => { + if (someCondition(error)) { + // Retry the request, returning the new observable + return forward(operation); + } + + // Log the error for any unhandled GraphQL errors or network errors. + console.log(`[Error]: ${error.message}`); + + // If nothing is returned from the error handler callback, the error will be + // emitted from the link chain as normal. +}); +``` + +### Retry Link + +Alternatively, you can use the `RetryLink` from `@apollo/client/link/retry` to implement retry logic for failed operations. + +```typescript +import { RetryLink } from "@apollo/client/link/retry"; + +const retryLink = new RetryLink({ + delay: { + initial: 300, + max: Infinity, + jitter: true, + }, + attempts: { + max: 5, + retryIf: (error, operation) => { + // Retry on network errors + return !!error && operation.operationName !== "SensitiveOperation"; + }, + }, +}); + +const client = new ApolloClient({ + cache: new InMemoryCache(), + link: from([retryLink, errorLink, httpLink]), +}); +``` + +### Custom Retry Logic + +```typescript +const retryLink = new RetryLink({ + attempts: (count, operation, error) => { + // Don't retry mutations + if (operation.query.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation")) { + return false; + } + + // Retry up to 3 times on network errors + return count < 3 && !!error; + }, + delay: (count) => { + // Exponential backoff + return Math.min(1000 * Math.pow(2, count), 30000); + }, +}); +``` + +## Error Boundaries + +When using suspenseful hooks, you should use React Error Boundaries for graceful error handling. + +### Non-suspense per-Component Error Handling + +```tsx +function SafeUserList() { + const { data, error, loading, refetch } = useQuery(GET_USERS, { + errorPolicy: "all", + notifyOnNetworkStatusChange: true, + }); + + // Handle network errors + if (error?.networkError) { + return ( + + Connection Error + Failed to load users. Please check your internet connection. + + + ); + } + + // Handle GraphQL errors but still show available data + return ( +
+ {error?.graphQLErrors && ( + Some data may be incomplete: {error.graphQLErrors[0].message} + )} + + {loading && } + + {data?.users && } +
+ ); +} +``` diff --git a/.agents/skills/apollo-client/references/fragments.md b/.agents/skills/apollo-client/references/fragments.md new file mode 100644 index 000000000..9df398061 --- /dev/null +++ b/.agents/skills/apollo-client/references/fragments.md @@ -0,0 +1,773 @@ +# Fragments Reference + +GraphQL fragments define a set of fields for a specific type. In Apollo Client, fragments are especially powerful when colocated with components to define each component's data requirements independently, creating a clear separation of concerns and enabling better component composition. + +## Table of Contents + +- [What Are Fragments](#what-are-fragments) +- [Basic Fragment Syntax](#basic-fragment-syntax) +- [Fragment Colocation](#fragment-colocation) +- [Fragment Reading Hooks](#fragment-reading-hooks) +- [Data Masking](#data-masking) +- [Fragment Registry](#fragment-registry) +- [TypeScript Integration](#typescript-integration) +- [Best Practices](#best-practices) + +## What Are Fragments + +A GraphQL fragment defines a set of fields for a specific GraphQL type. Fragments are defined on a specific GraphQL type and can be included in operations using the spread operator (`...`). + +In Apollo Client, fragments serve a specific purpose: + +**Fragments are for colocation, not reuse.** Each component should declare its data needs in a dedicated fragment. This allows components to independently evolve their data requirements without creating artificial dependencies between unrelated parts of your application. + +Fragments enable: + +1. **Component colocation**: Define the exact data requirements for a component alongside the component code +2. **Independent evolution**: Change a component's data needs without affecting other components +3. **Code organization**: Compose fragments together to build complete queries that mirror your component hierarchy + +## Basic Fragment Syntax + +### Defining a Fragment + +```typescript +import { gql } from "@apollo/client"; + +const USER_FRAGMENT = gql` + fragment UserFields on User { + id + name + email + avatarUrl + } +`; +``` + +Every fragment includes: + +- A unique name (`UserFields`) +- The type it operates on (`User`) +- The fields to select + +### Using Fragments in Queries + +Include fragments in queries using the spread operator: + +```typescript +const GET_USER = gql` + query GetUser($id: ID!) { + user(id: $id) { + ...UserFields + } + } + + ${USER_FRAGMENT} +`; +``` + +When using GraphQL Code Generator with the recommended configuration (typescript, typescript-operations, and typed-document-node plugins), fragments defined in your source files are automatically picked up and generated into typed document nodes. The generated fragment documents already include the fragment definition, so you don't need to interpolate them manually into queries. + +## Fragment Colocation + +Fragment colocation is the practice of defining fragments in the same file as the component that uses them. This creates a clear contract between components and their data requirements. + +### Why Colocate Fragments + +- **Locality**: Data requirements live next to the code that uses them +- **Maintainability**: Changes to component UI and data needs happen together +- **Type safety**: TypeScript can infer exact types from colocated fragments +- **Independence**: Components can evolve their data requirements without affecting other components + +### Colocation Pattern + +The recommended pattern for colocating fragments with components: + +```tsx +import { gql, FragmentType } from "@apollo/client"; +import { useSuspenseFragment } from "@apollo/client/react"; + +// Fragment definition +// This will be picked up by Codegen to create `UserCard_UserFragmentDoc` in `./fragments.generated.ts`. +// As that generated fragment document is correctly typed, we use that in the code going forward. +// This fragment will never be consumed in runtime code, so it is wrapped in `if (false)` so the bundler can omit it when bundling. +if (false) { + gql` + fragment UserCard_user on User { + id + name + email + avatarUrl + } + `; +} + +// This has been created from above fragment definition by CodeGen and is a correctly typed `TypedDocumentNode` +import { UserCard_UserFragmentDoc } from "./fragments.generated.ts"; + +// Component receives the (partially masked) parent object +export function UserCard({ user }: { user: FragmentType }) { + // Creates a subscription to the fragment in the cache + const { data } = useSuspenseFragment({ + fragment: UserCard_UserFragmentDoc, + fragmentName: "UserCard_user", + from: user, + }); + + return ( +
+ {data.name} +

{data.name}

+

{data.email}

+
+ ); +} +``` + +### Naming Convention + +A suggested naming pattern for fragments follows this convention: + +``` +{ComponentName}_{propName} +``` + +Where `propName` is the name of the prop the component receives containing the fragment data. + +Examples: + +- `UserCard_user` - Fragment for the `user` prop in the UserCard component +- `PostList_posts` - Fragment for the `posts` prop in the PostList component +- `CommentItem_comment` - Fragment for the `comment` prop in the CommentItem component + +This convention makes it clear which component owns which fragment. However, you can choose a different naming convention based on your project's needs. + +**Note**: A component might accept fragment data through multiple props, in which case it would have multiple associated fragments. For example, a `CommentCard` component might accept both a `comment` prop and an `author` prop, resulting in `CommentCard_comment` and `CommentCard_author` fragments. + +### Composing Fragments + +Parent components compose child fragments to build complete queries: + +```tsx +// Child component +import { gql } from "@apollo/client"; + +if (false) { + gql` + fragment UserAvatar_user on User { + id + avatarUrl + name + } + `; +} + +// Parent component composes child fragments +if (false) { + gql` + fragment UserProfile_user on User { + id + name + email + bio + ...UserAvatar_user + } + `; +} + +// Page-level query composes all fragments +if (false) { + gql` + query UserProfilePage($id: ID!) { + user(id: $id) { + ...UserProfile_user + } + } + `; +} +``` + +This creates a hierarchy that mirrors your component tree. + +## Fragment Reading Hooks + +Apollo Client provides hooks to read fragment data within components. These hooks work with data masking to ensure components only access the data they explicitly requested. + +### useSuspenseFragment + +For components using Suspense and concurrent features: + +```tsx +import { useSuspenseFragment } from "@apollo/client/react"; +import { FragmentType } from "@apollo/client"; +import { UserCard_UserFragmentDoc } from "./fragments.generated"; + +function UserCard({ user }: { user: FragmentType }) { + const { data } = useSuspenseFragment({ + fragment: UserCard_UserFragmentDoc, + fragmentName: "UserCard_user", + from: user, + }); + + return
{data.name}
; +} +``` + +### useFragment + +For components not using Suspense: + +```tsx +import { useFragment } from "@apollo/client/react"; +import { FragmentType } from "@apollo/client"; +import { UserCard_UserFragmentDoc } from "./fragments.generated"; + +function UserCard({ user }: { user: FragmentType }) { + const { data, complete } = useFragment({ + fragment: UserCard_UserFragmentDoc, + fragmentName: "UserCard_user", + from: user, + }); + + if (!complete) { + return
Loading...
; + } + + return
{data.name}
; +} +``` + +The `complete` field indicates whether all fragment data is available in the cache. + +### Hook Options + +Both hooks accept these options: + +```typescript +{ + // The fragment document (required) + fragment: TypedDocumentNode, + + // The fragment name (optional in most cases) + // Only required if the fragment document contains multiple definitions + fragmentName?: string, + + // The source data containing the fragment (required) + // Can be a single object or an array of objects + from: FragmentType | Array>, + + // Variables for the fragment (optional) + variables?: Variables, +} +``` + +When `from` is an array, the hook returns an array of results, allowing you to read fragments from multiple objects efficiently. **Note**: Array support for the `from` parameter was added in Apollo Client 4.1.0. + +## Data Masking + +Data masking is a feature that prevents components from accessing data they didn't explicitly request through their fragments. This enforces proper data boundaries and prevents over-rendering. + +### Enabling Data Masking + +Enable data masking when creating your Apollo Client: + +```typescript +import { ApolloClient, InMemoryCache } from "@apollo/client"; + +const client = new ApolloClient({ + cache: new InMemoryCache(), + dataMasking: true, // Enable data masking +}); +``` + +### How Data Masking Works + +With data masking enabled: + +1. Fragments return opaque `FragmentType` objects +2. Components must use `useFragment` or `useSuspenseFragment` to unmask data +3. Components can only access fields defined in their own fragments +4. TypeScript enforces these boundaries at compile time + +Without data masking: + +```tsx +// ❌ Without data masking - component can access any data from parent +function UserCard({ user }: { user: User }) { + // Can access any User field, even if not in fragment + return
{user.privateData}
; +} +``` + +With data masking: + +```tsx +// ✅ With data masking - component can only access its fragment data +import { UserCard_UserFragmentDoc } from "./fragments.generated"; + +function UserCard({ user }: { user: FragmentType }) { + const { data } = useSuspenseFragment({ + fragment: UserCard_UserFragmentDoc, + from: user, + }); + + // TypeScript error: 'privateData' doesn't exist on fragment type + // return
{data.privateData}
; + + // Only fields from the fragment are accessible + return
{data.name}
; +} +``` + +### Benefits of Data Masking + +- **Prevents over-rendering**: Components only re-render when their specific data changes +- **Enforces boundaries**: Components can't accidentally depend on data they don't own +- **Better refactoring**: Safe to modify parent queries without breaking child components +- **Type safety**: TypeScript catches attempts to access unavailable fields + +## Fragment Registry + +The fragment registry is an **alternative approach** to GraphQL Code Generator's automatic fragment inlining by name. It allows you to register fragments globally, making them available throughout your application by name reference. + +**Important**: GraphQL Code Generator automatically inlines fragments by name wherever they're used in your queries. Either approach is sufficient on its own—**you don't need to combine them**. + +### Creating a Fragment Registry + +```typescript +import { ApolloClient, InMemoryCache } from "@apollo/client"; +import { createFragmentRegistry } from "@apollo/client/cache"; + +export const fragmentRegistry = createFragmentRegistry(); + +const client = new ApolloClient({ + cache: new InMemoryCache({ + fragments: fragmentRegistry, + }), +}); +``` + +### Registering Fragments + +Register fragments after defining them: + +```typescript +import { gql } from "@apollo/client"; +import { fragmentRegistry } from "./apollo/client"; + +const USER_FRAGMENT = gql` + fragment UserFields on User { + id + name + email + } +`; + +fragmentRegistry.register(USER_FRAGMENT); +``` + +With colocated fragments: + +```tsx +import { fragmentRegistry } from "@/apollo/client"; +import { UserCard_UserFragmentDoc } from "./fragments.generated"; + +// Register the fragment globally +fragmentRegistry.register(UserCard_UserFragmentDoc); +``` + +### Using Registered Fragments + +Once registered, fragments can be referenced by name in queries without explicit imports: + +```tsx +// Fragment is available by name because it's registered +const GET_USER = gql` + query GetUser($id: ID!) { + user(id: $id) { + ...UserCard_user + } + } +`; +``` + +### Approaches for Fragment Composition + +There are three approaches to make child fragments available in parent queries: + +1. **GraphQL Code Generator inlining** (Recommended): CodeGen automatically inlines fragments by name. No manual work needed—just reference fragments by name in your queries. + +2. **Fragment Registry**: Manually register fragments to make them available by name. Useful for runtime scenarios where CodeGen isn't available. + +3. **Manual interpolation**: Explicitly import and interpolate child fragments into parent fragments: + + ```typescript + import { CHILD_FRAGMENT } from "./ChildComponent"; + + const PARENT_FRAGMENT = gql` + fragment Parent_data on Data { + field + ...Child_data + } + ${CHILD_FRAGMENT} + `; + ``` + +### Pros and Cons + +**GraphQL Code Generator inlining**: + +- ✅ Less work: Automatic, no manual registration needed +- ❌ Larger bundle: Fragments are inlined into every query that uses them + +**Fragment Registry**: + +- ✅ Smaller bundle: Fragments are registered once, referenced by name +- ❌ More work: Requires manual registration of each fragment +- ❌ May cause issues with lazy-loaded modules if the module is not loaded before the query is executed +- ✅ Best for deeply nested component trees where bundle size matters + +**Manual interpolation**: + +- ❌ Most work: Manual imports and interpolation required +- ✅ Explicit: Clear fragment dependencies in code + +### Recommendation + +For most applications using GraphQL Code Generator (as shown in this guide), **use the automatic inlining**—it requires no additional setup and works seamlessly. Consider the fragment registry only if bundle size becomes a concern in applications with deeply nested component trees. + +## TypeScript Integration + +Apollo Client provides strong TypeScript support for fragments through GraphQL Code Generator. + +### Generated Types + +GraphQL Code Generator produces typed fragment documents: + +```typescript +// Generated file: fragments.generated.ts +export type UserCard_UserFragment = { + __typename: "User"; + id: string; + name: string; + email: string; + avatarUrl: string; +} & { " $fragmentName"?: "UserCard_UserFragment" }; + +export const UserCard_UserFragmentDoc: TypedDocumentNode; +``` + +### Type-Safe Fragment Usage + +Use `FragmentType` to accept masked fragment data: + +```tsx +import { FragmentType } from "@apollo/client"; +import { UserCard_UserFragmentDoc } from "./fragments.generated"; + +function UserCard({ user }: { user: FragmentType }) { + const { data } = useSuspenseFragment({ + fragment: UserCard_UserFragmentDoc, + from: user, + }); + + // 'data' is fully typed as UserCard_UserFragment + return
{data.name}
; +} +``` + +### Fragment Type Inference + +TypeScript infers types from fragment documents automatically: + +```tsx +import { UserCard_UserFragmentDoc } from "./fragments.generated"; + +// Types are inferred from the fragment +const { data } = useSuspenseFragment({ + fragment: UserCard_UserFragmentDoc, + from: user, +}); + +// data.name is string +// data.email is string +// data.nonExistentField is a TypeScript error +``` + +### Parent-Child Type Safety + +When passing fragment data from parent to child: + +```tsx +// Parent query +const { data } = useSuspenseQuery(GET_USER); + +// TypeScript ensures the query includes UserCard_user fragment +// before allowing it to be passed to UserCard +; +``` + +## Best Practices + +### Prefer Colocation Over Reuse + +**Fragments are for colocation, not reuse.** Each component should declare its data needs in a dedicated fragment, even if multiple components currently need the same fields. + +Sharing fragments between components just because they happen to need the same fields today creates artificial dependencies. When one component's requirements change, the shared fragment must be updated, causing all components using it to over-fetch data they don't need. + +```tsx +// ✅ Good: Each component has its own fragment +if (false) { + gql` + fragment UserCard_user on User { + id + name + email + avatarUrl + } + `; + + gql` + fragment UserListItem_user on User { + id + name + email + } + `; +} + +// If UserCard later needs 'bio', only UserCard_user changes +// UserListItem doesn't over-fetch 'bio' +``` + +```tsx +// ❌ Avoid: Sharing a generic fragment across components +const COMMON_USER_FIELDS = gql` + fragment CommonUserFields on User { + id + name + email + } +`; + +// UserCard and UserListItem both use CommonUserFields +// When UserCard needs 'bio', adding it to CommonUserFields +// causes UserListItem to over-fetch unnecessarily +``` + +This independence allows each component to evolve its data requirements without affecting unrelated parts of your application. + +### One Query Per Page + +Compose all page data requirements into a single query at the page level: + +```tsx +// ✅ Good: Single page-level query +if (false) { + gql` + query UserProfilePage($id: ID!) { + user(id: $id) { + ...UserHeader_user + ...UserPosts_user + ...UserFriends_user + } + } + `; +} +``` + +```tsx +// ❌ Avoid: Multiple queries in different components +function UserProfile() { + const { data: userData } = useQuery(GET_USER); + const { data: postsData } = useQuery(GET_USER_POSTS); + const { data: friendsData } = useQuery(GET_USER_FRIENDS); + // ... +} +``` + +### Use Fragment-Reading Hooks in Components + +Non-page components should use `useFragment` or `useSuspenseFragment`: + +```tsx +// ✅ Good: Component reads fragment data +import { FragmentType } from "@apollo/client"; +import { useSuspenseFragment } from "@apollo/client/react"; +import { UserCard_UserFragmentDoc } from "./fragments.generated"; + +function UserCard({ user }: { user: FragmentType }) { + const { data } = useSuspenseFragment({ + fragment: UserCard_UserFragmentDoc, + from: user, + }); + return
{data.name}
; +} +``` + +```tsx +// ❌ Avoid: Component uses query hook +function UserCard({ userId }: { userId: string }) { + const { data } = useQuery(GET_USER, { variables: { id: userId } }); + return
{data.user.name}
; +} +``` + +### Request Only Required Fields + +Keep fragments minimal and only request fields the component actually uses: + +```tsx +// ✅ Good: Only necessary fields +if (false) { + gql` + fragment UserListItem_user on User { + id + name + } + `; +} +``` + +```tsx +// ❌ Avoid: Requesting unused fields +if (false) { + gql` + fragment UserListItem_user on User { + id + name + email + bio + friends { + id + name + } + posts { + id + title + } + } + `; +} +``` + +### Use @defer for Below-the-Fold Content + +The `@defer` directive allows you to defer loading of non-critical fields, enabling faster initial page loads by prioritizing essential data. The deferred fields are delivered via incremental delivery and arrive after the non-deferred data, allowing the UI to progressively render as data becomes available. + +Defer slow fields that aren't immediately visible: + +```tsx +if (false) { + gql` + query ProductPage($id: ID!) { + product(id: $id) { + id + name + price + ...ProductReviews_product @defer + } + } + `; +} +``` + +This allows the page to render quickly while reviews load in the background. + +### Handle Client-Only Fields + +Use the `@client` directive for fields resolved locally: + +```tsx +if (false) { + gql` + fragment TodoItem_todo on Todo { + id + text + completed + isSelected @client + } + `; +} +``` + +### Enable Data Masking for New Applications + +Always enable data masking in new applications: + +```typescript +const client = new ApolloClient({ + cache: new InMemoryCache(), + dataMasking: true, +}); +``` + +This enforces proper boundaries from the start and prevents accidental coupling between components. + +## Apollo Client Data Masking vs GraphQL-Codegen Fragment Masking + +Apollo Client's data masking and GraphQL Code Generator's fragment masking are different features that serve different purposes: + +### GraphQL-Codegen Fragment Masking + +GraphQL Code Generator's fragment masking (when using the client preset) is a **type-level** feature: + +- Masks data only at the TypeScript type level +- The actual runtime data remains fully accessible on the object +- Using their `useFragment` hook simply "unmasks" the data on a type level +- Does not prevent accidental access to data at runtime +- Parent components receive all data and pass it down +- This means the parent component has to be subscribed to all data + +### Apollo Client Data Masking + +Apollo Client's data masking is a **runtime** feature with significant performance benefits: + +- Removes data at the runtime level, not just the type level +- The `useFragment` and `useSuspenseFragment` hooks create cache subscriptions +- Parent objects are sparse and only contain unmasked data +- Prevents accidental access to data that should be masked + +### Key Benefits of Apollo Client Data Masking + +**1. No Accidental Data Access** + +With runtime data masking, masked fields are not present in the parent object at all. You cannot accidentally access them, even if you bypass TypeScript type checking. + +**2. Fewer Re-renders** + +Apollo Client's approach creates more efficient subscriptions: + +- **Without data masking**: Parent component subscribes to all fields (including masked ones). When a masked child field changes, the parent re-renders to pass that runtime data down the tree. +- **With data masking**: Parent component only subscribes to its own unmasked fields. Subscriptions on masked fields happen lower in the React component tree when the child component calls `useSuspenseFragment`. When a masked field changes, only the child component that subscribed to it re-renders. + +### Example + +```tsx +import { FragmentType } from "@apollo/client"; +import { useSuspenseQuery, useSuspenseFragment } from "@apollo/client/react"; +import { UserCard_UserFragmentDoc } from "./fragments.generated"; + +function ParentComponent() { + const { data } = useSuspenseQuery(GET_USER); + + // With Apollo Client data masking: + // - data.user only contains unmasked fields + // - Parent doesn't re-render when child-specific fields change + + return ; +} + +function UserCard({ user }: { user: FragmentType }) { + // Creates a cache subscription specifically for UserCard_user fields + const { data } = useSuspenseFragment({ + fragment: UserCard_UserFragmentDoc, + from: user, + }); + + // Only this component re-renders when these fields change + return
{data.name}
; +} +``` + +This granular subscription approach improves performance in large applications with deeply nested component trees. diff --git a/.agents/skills/apollo-client/references/integration-client.md b/.agents/skills/apollo-client/references/integration-client.md new file mode 100644 index 000000000..56ffef3b9 --- /dev/null +++ b/.agents/skills/apollo-client/references/integration-client.md @@ -0,0 +1,337 @@ +# Apollo Client Integration for Client-Side Apps + +This guide covers setting up Apollo Client in client-side React applications without server-side rendering (SSR). This includes applications using Vite, Parcel, Create React App, or other bundlers that don't implement SSR. + +For applications with SSR, use one of the framework-specific integration guides instead: + +- [Next.js App Router](integration-nextjs.md) +- [React Router Framework Mode](integration-react-router.md) +- [TanStack Start](integration-tanstack-start.md) + +## Installation + +```bash +npm install @apollo/client graphql rxjs +``` + +## TypeScript Code Generation (optional but recommended) + +For type-safe GraphQL operations with TypeScript, see the [TypeScript Code Generation guide](typescript-codegen.md). + +## Setup Steps + +### Step 1: Create Client + +```typescript +import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client"; + +// Recommended: Use HttpOnly cookies for authentication +const httpLink = new HttpLink({ + uri: "https://your-graphql-endpoint.com/graphql", + credentials: "include", // Sends cookies with requests (secure when using HttpOnly cookies) +}); + +const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), +}); +``` + +If you need manual token management (less secure, only when HttpOnly cookies aren't available): + +```typescript +import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client"; +import { SetContextLink } from "@apollo/client/link/context"; + +const httpLink = new HttpLink({ + uri: "https://your-graphql-endpoint.com/graphql", +}); + +const authLink = new SetContextLink(({ headers }) => { + const token = localStorage.getItem("token"); + return { + headers: { + ...headers, + authorization: token ? `Bearer ${token}` : "", + }, + }; +}); + +const client = new ApolloClient({ + link: authLink.concat(httpLink), + cache: new InMemoryCache(), +}); +``` + +### Step 2: Setup Provider + +```tsx +import { ApolloProvider } from "@apollo/client"; +import App from "./App"; + +function Root() { + return ( + + + + ); +} +``` + +### Step 3: Execute Query + +```tsx +import { gql } from "@apollo/client"; +import { useQuery } from "@apollo/client/react"; + +const GET_USERS = gql` + query GetUsers { + users { + id + name + email + } + } +`; + +function UserList() { + const { loading, error, data, dataState } = useQuery(GET_USERS); + + if (loading) return

Loading...

; + if (error) return

Error: {error.message}

; + + // TypeScript: dataState === "ready" provides better type narrowing than just checking data + return ( +
    + {data.users.map((user) => ( +
  • {user.name}
  • + ))} +
+ ); +} +``` + +## Basic Query Usage + +### Using Variables + +```tsx +const GET_USER = gql` + query GetUser($id: ID!) { + user(id: $id) { + id + name + email + } + } +`; + +function UserProfile({ userId }: { userId: string }) { + const { loading, error, data, dataState } = useQuery(GET_USER, { + variables: { id: userId }, + }); + + if (loading) return

Loading...

; + if (error) return

Error: {error.message}

; + + // TypeScript: dataState === "ready" provides better type narrowing than just checking data + return
{data.user.name}
; +} +``` + +> **Note for TypeScript users**: Use [`dataState`](https://www.apollographql.com/docs/react/data/typescript#type-narrowing-data-with-datastate) for more robust type safety and better type narrowing in Apollo Client 4.x. + +### TypeScript Integration + +For complete examples with loading, error handling, and `dataState` for type narrowing, see [Basic Query Usage](#basic-query-usage) above. + +#### Usage with Generated Types + +For type-safe operations with code generation, see the [TypeScript Code Generation guide](typescript-codegen.md). + +Quick example: + +```typescript +import { gql } from "@apollo/client"; +import { useQuery } from "@apollo/client/react"; +import { GetUserDocument } from "./queries.generated"; + +// Define your query with the if (false) pattern for code generation +if (false) { + gql` + query GetUser($id: ID!) { + user(id: $id) { + id + name + email + } + } + `; +} + +function UserProfile({ userId }: { userId: string }) { + // Types are automatically inferred from GetUserDocument + const { data } = useQuery(GetUserDocument, { + variables: { id: userId }, + }); + + return
{data.user.name}
; +} +``` + +#### Usage with Manual Type Annotations + +If not using code generation, define types alongside your queries using `TypedDocumentNode`: + +```typescript +import { gql, TypedDocumentNode } from "@apollo/client"; +import { useQuery } from "@apollo/client/react"; + +interface GetUserData { + user: { + id: string; + name: string; + email: string; + }; +} + +interface GetUserVariables { + id: string; +} + +// Types should always be defined via TypedDocumentNode alongside your queries/mutations, not at the useQuery/useMutation call site +const GET_USER: TypedDocumentNode = gql` + query GetUser($id: ID!) { + user(id: $id) { + id + name + email + } + } +`; + +function UserProfile({ userId }: { userId: string }) { + const { data } = useQuery(GET_USER, { + variables: { id: userId }, + }); + + // data.user is automatically typed from GET_USER + return
{data.user.name}
; +} +``` + +## Basic Mutation Usage + +```tsx +import { gql, TypedDocumentNode } from "@apollo/client"; +import { useMutation } from "@apollo/client/react"; + +interface CreateUserMutation { + createUser: { + id: string; + name: string; + email: string; + }; +} + +interface CreateUserMutationVariables { + input: { + name: string; + email: string; + }; +} + +const CREATE_USER: TypedDocumentNode = gql` + mutation CreateUser($input: CreateUserInput!) { + createUser(input: $input) { + id + name + email + } + } +`; + +function CreateUserForm() { + const [createUser, { loading, error }] = useMutation(CREATE_USER); + + const handleSubmit = async (formData: FormData) => { + const { data } = await createUser({ + variables: { + input: { + name: formData.get("name") as string, + email: formData.get("email") as string, + }, + }, + }); + if (data) { + console.log("Created user:", data.createUser); + } + }; + + return ( +
{ + e.preventDefault(); + handleSubmit(new FormData(e.currentTarget)); + }} + > + + + + {error &&

Error: {error.message}

} +
+ ); +} +``` + +## Client Configuration Options + +```typescript +const client = new ApolloClient({ + // Required: The cache implementation + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + // Field-level cache configuration + }, + }, + }, + }), + + // Network layer + link: new HttpLink({ uri: "/graphql" }), + + // Avoid defaultOptions if possible as they break TypeScript expectations. + // Configure options per-query/mutation instead for better type safety. + // defaultOptions: { + // watchQuery: { fetchPolicy: 'cache-and-network' }, + // }, + + // DevTools are enabled by default in development + // Only configure when enabling in production + devtools: { + enabled: true, // Only needed for production + }, + + // Custom name for this client instance + clientAwareness: { + name: "web-client", + version: "1.0.0", + }, +}); +``` + +## Important Considerations + +1. **Choose Your Hook Strategy:** Decide if your application should be based on Suspense. If it is, use suspenseful hooks like `useSuspenseQuery` (see [Suspense Hooks guide](suspense-hooks.md)), otherwise use non-suspenseful hooks like `useQuery` (see [Queries guide](queries.md)). + +2. **Client-Side Only:** This setup is for client-side apps without SSR. The Apollo Client instance is created once and reused throughout the application lifecycle. + +3. **Authentication:** Prefer HttpOnly cookies with `credentials: "include"` in `HttpLink` options to avoid exposing tokens to JavaScript. If manual token management is necessary, use `SetContextLink` to dynamically add authentication headers from `localStorage` or other client-side storage. + +4. **Environment Variables:** Store your GraphQL endpoint URL in environment variables for different environments (development, staging, production). + +5. **Error Handling:** Always handle `loading` and `error` states when using `useQuery` or `useLazyQuery`. For Suspense-based hooks (`useSuspenseQuery`), React handles this through `` boundaries and error boundaries. diff --git a/.agents/skills/apollo-client/references/integration-nextjs.md b/.agents/skills/apollo-client/references/integration-nextjs.md new file mode 100644 index 000000000..9dfb0a8b2 --- /dev/null +++ b/.agents/skills/apollo-client/references/integration-nextjs.md @@ -0,0 +1,317 @@ +# Apollo Client Integration with Next.js App Router + +This guide covers integrating Apollo Client in a Next.js application using the App Router architecture with support for both React Server Components (RSC) and Client Components. + +## What is supported? + +### React Server Components + +Apollo Client provides a shared client instance across all server components for a single request, preventing duplicate GraphQL requests and optimizing server-side rendering. + +### React Client Components + +When using the `app` directory, client components are rendered both on the server (SSR) and in the browser. Apollo Client enables you to execute GraphQL queries on the server and use the results to hydrate your browser-side cache, delivering fully-rendered pages to users. + +## Installation + +Install Apollo Client and the Next.js integration package: + +```bash +npm install @apollo/client@latest @apollo/client-integration-nextjs graphql rxjs +``` + +> **TypeScript users:** For type-safe GraphQL operations, see the [TypeScript Code Generation guide](typescript-codegen.md). + +## Setup for React Server Components (RSC) + +### Step 1: Create Apollo Client Configuration + +Create an `ApolloClient.ts` file in your app directory: + +```typescript +import { HttpLink } from "@apollo/client"; +import { registerApolloClient, ApolloClient, InMemoryCache } from "@apollo/client-integration-nextjs"; + +export const { getClient, query, PreloadQuery } = registerApolloClient(() => { + return new ApolloClient({ + cache: new InMemoryCache(), + link: new HttpLink({ + // Use an absolute URL for SSR (relative URLs cannot be used in SSR) + uri: "https://your-api.com/graphql", + fetchOptions: { + // Optional: Next.js-specific fetch options for caching and revalidation + // See: https://nextjs.org/docs/app/api-reference/functions/fetch + }, + }), + }); +}); +``` + +### Step 2: Use in Server Components + +You can now use the `getClient` function or the `query` shortcut in your server components: + +```typescript +import { query } from "./ApolloClient"; + +async function UserProfile({ userId }: { userId: string }) { + const { data } = await query({ + query: GET_USER, + variables: { id: userId }, + }); + + return
{data.user.name}
; +} +``` + +### Override Next.js Fetch Options + +You can override Next.js-specific `fetch` options per query using `context.fetchOptions`: + +```typescript +const { data } = await getClient().query({ + query: GET_USER, + context: { + fetchOptions: { + next: { revalidate: 60 }, // Revalidate every 60 seconds + }, + }, +}); +``` + +## Setup for Client Components (SSR and Browser) + +### Step 1: Create Apollo Wrapper Component + +Create `app/ApolloWrapper.tsx`: + +```typescript +"use client"; + +import { HttpLink } from "@apollo/client"; +import { + ApolloNextAppProvider, + ApolloClient, + InMemoryCache, +} from "@apollo/client-integration-nextjs"; + +function makeClient() { + const httpLink = new HttpLink({ + // Use an absolute URL for SSR + uri: "https://your-api.com/graphql", + fetchOptions: { + // Optional: Next.js-specific fetch options + // Note: This doesn't work with `export const dynamic = "force-static"` + }, + }); + + return new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + }); +} + +export function ApolloWrapper({ children }: React.PropsWithChildren) { + return ( + + {children} + + ); +} +``` + +### Step 2: Wrap Root Layout + +Wrap your `RootLayout` in the `ApolloWrapper` component in `app/layout.tsx`: + +```typescript +import { ApolloWrapper } from "./ApolloWrapper"; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} +``` + +> **Note:** This works even if your layout is a React Server Component. It ensures all Client Components share the same Apollo Client instance through `ApolloNextAppProvider`. + +### Step 3: Use Apollo Client Hooks in Client Components + +For optimal streaming SSR, use suspense-enabled hooks like `useSuspenseQuery` and `useFragment`: + +```typescript +"use client"; + +import { useSuspenseQuery } from "@apollo/client/react"; + +export function UserProfile({ userId }: { userId: string }) { + const { data } = useSuspenseQuery(GET_USER, { + variables: { id: userId }, + }); + + return
{data.user.name}
; +} +``` + +## Preloading Data from RSC to Client Components + +You can preload data in React Server Components to populate the cache of your Client Components. + +### Step 1: Use PreloadQuery in Server Components + +```tsx +import { PreloadQuery } from "./ApolloClient"; +import { Suspense } from "react"; + +export default async function Page() { + return ( + + Loading...}> + + + + ); +} +``` + +### Step 2: Consume with useSuspenseQuery in Client Components + +```tsx +"use client"; + +import { useSuspenseQuery } from "@apollo/client/react"; + +export function ClientChild() { + const { data } = useSuspenseQuery(GET_USER, { + variables: { id: "1" }, + }); + + return
{data.user.name}
; +} +``` + +> **Important:** Data fetched this way should be considered client data and never referenced in Server Components. `PreloadQuery` prevents mixing server data and client data by creating a separate `ApolloClient` instance. + +### Using with useReadQuery + +For advanced use cases, you can use `PreloadQuery` with `useReadQuery` to avoid request waterfalls: + +```tsx + + {(queryRef) => ( + Loading...}> + + + )} + +``` + +In your Client Component: + +```tsx +"use client"; + +import { useQueryRefHandlers, useReadQuery, QueryRef } from "@apollo/client/react"; + +export function ClientChild({ queryRef }: { queryRef: QueryRef }) { + const { refetch } = useQueryRefHandlers(queryRef); + const { data } = useReadQuery(queryRef); + + return
{data.user.name}
; +} +``` + +## Handling Multipart Responses (@defer) in SSR + +When using the `@defer` directive, `useSuspenseQuery` will only suspend until the initial response is received. To handle deferred data properly, you have three strategies: + +### Strategy 1: Use PreloadQuery with useReadQuery + +`PreloadQuery` allows deferred data to be fully transported and streamed chunk-by-chunk. + +### Strategy 2: Remove @defer Fragments + +Use `RemoveMultipartDirectivesLink` to strip `@defer` directives from queries during SSR: + +```typescript +import { RemoveMultipartDirectivesLink } from "@apollo/client-integration-nextjs"; + +new RemoveMultipartDirectivesLink({ + stripDefer: true, // Default: true +}); +``` + +You can exclude specific fragments from stripping by labeling them: + +```graphql +query myQuery { + fastField + ... @defer(label: "SsrDontStrip1") { + slowField1 + } +} +``` + +### Strategy 3: Wait for Deferred Data + +Use `AccumulateMultipartResponsesLink` to debounce the initial response: + +```typescript +import { AccumulateMultipartResponsesLink } from "@apollo/client-integration-nextjs"; + +new AccumulateMultipartResponsesLink({ + cutoffDelay: 100, // Wait up to 100ms for incremental data +}); +``` + +### Combined Approach: SSRMultipartLink + +Combine both strategies with `SSRMultipartLink`: + +```typescript +import { SSRMultipartLink } from "@apollo/client-integration-nextjs"; + +new SSRMultipartLink({ + stripDefer: true, + cutoffDelay: 100, +}); +``` + +## Testing + +Reset singleton instances between tests using the `resetApolloClientSingletons` helper: + +```typescript +import { resetApolloClientSingletons } from "@apollo/client-integration-nextjs"; + +afterEach(resetApolloClientSingletons); +``` + +## Debugging + +Enable verbose logging in your `app/ApolloWrapper.tsx`: + +```typescript +import { setLogVerbosity } from "@apollo/client"; + +setLogVerbosity("debug"); +``` + +## Important Considerations + +1. **Separate RSC and SSR Queries:** Avoid overlapping queries between RSC and SSR. RSC queries don't update in the browser, while SSR queries can update dynamically as the cache changes. + +2. **Use Absolute URLs:** Always use absolute URLs in `HttpLink` for SSR, as relative URLs cannot be used in server-side rendering. + +3. **Streaming SSR:** For optimal performance, use `useSuspenseQuery` and `useFragment` to take advantage of React 18's streaming SSR capabilities. + +4. **Suspense Boundaries:** Place `Suspense` boundaries at meaningful places in your UI for the best user experience. diff --git a/.agents/skills/apollo-client/references/integration-react-router.md b/.agents/skills/apollo-client/references/integration-react-router.md new file mode 100644 index 000000000..627d58fd6 --- /dev/null +++ b/.agents/skills/apollo-client/references/integration-react-router.md @@ -0,0 +1,253 @@ +# Apollo Client Integration with React Router Framework Mode + +This guide covers integrating Apollo Client in a React Router 7 application with support for modern streaming SSR. + +## Installation + +Install Apollo Client and the React Router integration package: + +```bash +npm install @apollo/client-integration-react-router @apollo/client graphql rxjs +``` + +> **TypeScript users:** For type-safe GraphQL operations, see the [TypeScript Code Generation guide](typescript-codegen.md). + +## Setup + +### Step 1: Create Apollo Configuration + +Create an `app/apollo.ts` file that exports a `makeClient` function and an `apolloLoader`: + +```typescript +import { HttpLink, InMemoryCache } from "@apollo/client"; +import { createApolloLoaderHandler, ApolloClient } from "@apollo/client-integration-react-router"; + +// `request` will be available on the server during SSR or in loaders, but not in the browser +export const makeClient = (request?: Request) => { + return new ApolloClient({ + cache: new InMemoryCache(), + link: new HttpLink({ uri: "https://your-graphql-endpoint.com/graphql" }), + }); +}; + +export const apolloLoader = createApolloLoaderHandler(makeClient); +``` + +> **Important:** `ApolloClient` must be imported from `@apollo/client-integration-react-router`, not from `@apollo/client`. + +### Step 2: Reveal Entry Files + +Run the following command to create the entry files if they don't exist: + +```bash +npx react-router reveal +``` + +This will create `app/entry.client.tsx` and `app/entry.server.tsx`. + +### Step 3: Configure Client Entry + +Adjust `app/entry.client.tsx` to wrap your app in `ApolloProvider`: + +```typescript +import { makeClient } from "./apollo"; +import { ApolloProvider } from "@apollo/client"; +import { StrictMode, startTransition } from "react"; +import { hydrateRoot } from "react-dom/client"; +import { HydratedRouter } from "react-router/dom"; + +startTransition(() => { + const client = makeClient(); + hydrateRoot( + document, + + + + + + ); +}); +``` + +### Step 4: Configure Server Entry + +Adjust `app/entry.server.tsx` to wrap your app in `ApolloProvider` during SSR: + +```typescript +import { makeClient } from "./apollo"; +import { ApolloProvider } from "@apollo/client"; +// ... other imports + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext +) { + return new Promise((resolve, reject) => { + // ... existing code + + const client = makeClient(request); + + const { pipe, abort } = renderToPipeableStream( + + + , + { + [readyOption]() { + shellRendered = true; + // ... rest of the handler + }, + // ... other options + } + ); + }); +} +``` + +### Step 5: Add Hydration Helper + +Add `` to `app/root.tsx`: + +```typescript +import { ApolloHydrationHelper } from "@apollo/client-integration-react-router"; +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} +``` + +## Usage + +### Using apolloLoader with useReadQuery + +You can now use the `apolloLoader` function to create Apollo-enabled loaders for your routes: + +```typescript +import { gql } from "@apollo/client"; +import { useReadQuery } from "@apollo/client/react"; +import { useLoaderData } from "react-router"; +import type { Route } from "./+types/my-route"; +import type { TypedDocumentNode } from "@apollo/client"; +import { apolloLoader } from "./apollo"; + +// TypedDocumentNode definition with types +const GET_USER: TypedDocumentNode< + { user: { id: string; name: string; email: string } }, + { id: string } +> = gql` + query GetUser($id: ID!) { + user(id: $id) { + id + name + email + } + } +`; + +export const loader = apolloLoader()(({ preloadQuery }) => { + const userQueryRef = preloadQuery(GET_USER, { + variables: { id: "1" }, + }); + + return { + userQueryRef, + }; +}); + +export default function UserPage() { + const { userQueryRef } = useLoaderData(); + const { data } = useReadQuery(userQueryRef); + + return ( +
+

{data.user.name}

+

{data.user.email}

+
+ ); +} +``` + +> **Important:** To provide better TypeScript support, `apolloLoader` is a method that you need to call twice: `apolloLoader()(loader)` + +### Multiple Queries in a Loader + +You can preload multiple queries in a single loader: + +```typescript +import { gql } from "@apollo/client"; +import { useReadQuery } from "@apollo/client/react"; +import { useLoaderData } from "react-router"; +import type { Route } from "./+types/my-route"; +import { apolloLoader } from "./apollo"; + +// TypedDocumentNode definitions omitted for brevity + +export const loader = apolloLoader()(({ preloadQuery }) => { + const userQueryRef = preloadQuery(GET_USER, { + variables: { id: "1" }, + }); + + const postsQueryRef = preloadQuery(GET_POSTS, { + variables: { userId: "1" }, + }); + + return { + userQueryRef, + postsQueryRef, + }; +}); + +export default function UserPage() { + const { userQueryRef, postsQueryRef } = useLoaderData(); + const { data: userData } = useReadQuery(userQueryRef); + const { data: postsData } = useReadQuery(postsQueryRef); + + return ( +
+

{userData.user.name}

+

Posts

+
    + {postsData.posts.map((post) => ( +
  • {post.title}
  • + ))} +
+
+ ); +} +``` + +## Important Considerations + +1. **Import ApolloClient from Integration Package:** Always import `ApolloClient` from `@apollo/client-integration-react-router`, not from `@apollo/client`, to ensure proper SSR hydration. + +2. **TypeScript Support:** The `apolloLoader` function requires double invocation for proper TypeScript type inference: `apolloLoader()(loader)`. + +3. **Request Context:** The `makeClient` function receives the `Request` object during SSR and in loaders, but not in the browser. Use this to set up auth headers or other request-specific configuration. + +4. **Streaming SSR:** The integration fully supports React's streaming SSR capabilities. Place `Suspense` boundaries strategically for optimal user experience. + +5. **Cache Hydration:** The `ApolloHydrationHelper` component ensures that data loaded on the server is properly hydrated on the client, preventing unnecessary refetches. diff --git a/.agents/skills/apollo-client/references/integration-tanstack-start.md b/.agents/skills/apollo-client/references/integration-tanstack-start.md new file mode 100644 index 000000000..a3ecbfeae --- /dev/null +++ b/.agents/skills/apollo-client/references/integration-tanstack-start.md @@ -0,0 +1,365 @@ +# Apollo Client Integration with TanStack Start + +This guide covers integrating Apollo Client in a TanStack Start application with support for modern streaming SSR. + +> **Note:** When using `npx create-tsrouter-app` to create a new TanStack Start application, you can choose Apollo Client in the setup wizard to have all of this configuration automatically set up for you. + +## Installation + +Install Apollo Client and the TanStack Start integration package: + +```bash +npm install @apollo/client-integration-tanstack-start @apollo/client graphql rxjs +``` + +> **TypeScript users:** For type-safe GraphQL operations, see the [TypeScript Code Generation guide](typescript-codegen.md). + +## Setup + +### Step 1: Configure Root Route with Context + +In your `routes/__root.tsx`, change from `createRootRoute` to `createRootRouteWithContext` to provide the right context type: + +```typescript +import type { ApolloClientIntegration } from "@apollo/client-integration-tanstack-start"; +import { + createRootRouteWithContext, + Outlet, +} from "@tanstack/react-router"; + +export const Route = createRootRouteWithContext()({ + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + + My App + + + + + + ); +} +``` + +### Step 2: Set Up Apollo Client in Router + +In your `router.tsx`, set up your Apollo Client instance and run `routerWithApolloClient`: + +```typescript +import { routerWithApolloClient, ApolloClient, InMemoryCache } from "@apollo/client-integration-tanstack-start"; +import { HttpLink } from "@apollo/client"; +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; + +export function getRouter() { + const apolloClient = new ApolloClient({ + cache: new InMemoryCache(), + link: new HttpLink({ uri: "https://your-graphql-endpoint.com/graphql" }), + }); + + const router = createRouter({ + routeTree, + context: { + ...routerWithApolloClient.defaultContext, + }, + }); + + return routerWithApolloClient(router, apolloClient); +} +``` + +> **Important:** `ApolloClient` and `InMemoryCache` must be imported from `@apollo/client-integration-tanstack-start`, not from `@apollo/client`. + +## Usage + +### Option 1: Loader with preloadQuery and useReadQuery + +Use the `preloadQuery` function in your route loader to preload data during navigation: + +```typescript +import { gql } from "@apollo/client"; +import { useReadQuery } from "@apollo/client/react"; +import { createFileRoute } from "@tanstack/react-router"; +import type { TypedDocumentNode } from "@apollo/client"; + +// TypedDocumentNode definition with types +const GET_USER: TypedDocumentNode< + { user: { id: string; name: string; email: string } }, + { id: string } +> = gql` + query GetUser($id: ID!) { + user(id: $id) { + id + name + email + } + } +`; + +export const Route = createFileRoute("/user/$userId")({ + component: RouteComponent, + loader: ({ context: { preloadQuery }, params }) => { + const queryRef = preloadQuery(GET_USER, { + variables: { id: params.userId }, + }); + + return { + queryRef, + }; + }, +}); + +function RouteComponent() { + const { queryRef } = Route.useLoaderData(); + const { data } = useReadQuery(queryRef); + + return ( +
+

{data.user.name}

+

{data.user.email}

+
+ ); +} +``` + +### Option 2: Direct useSuspenseQuery in Component + +You can also use Apollo Client's suspenseful hooks directly in your component without a loader: + +```typescript +import { gql, useSuspenseQuery } from "@apollo/client/react"; +import { createFileRoute } from "@tanstack/react-router"; +import type { TypedDocumentNode } from "@apollo/client"; + +// TypedDocumentNode definition with types +const GET_POSTS: TypedDocumentNode<{ + posts: Array<{ id: string; title: string; content: string }>; +}> = gql` + query GetPosts { + posts { + id + title + content + } + } +`; + +export const Route = createFileRoute("/posts")({ + component: RouteComponent, +}); + +function RouteComponent() { + const { data } = useSuspenseQuery(GET_POSTS); + + return ( +
+

Posts

+
    + {data.posts.map((post) => ( +
  • +

    {post.title}

    +

    {post.content}

    +
  • + ))} +
+
+ ); +} +``` + +### Multiple Queries in a Loader + +You can preload multiple queries in a single loader: + +```typescript +import { gql } from "@apollo/client"; +import { useReadQuery } from "@apollo/client/react"; +import { createFileRoute } from "@tanstack/react-router"; + +// TypedDocumentNode definitions omitted for brevity + +export const Route = createFileRoute("/dashboard")({ + component: RouteComponent, + loader: ({ context: { preloadQuery } }) => { + const userQueryRef = preloadQuery(GET_USER, { + variables: { id: "current" }, + }); + + const statsQueryRef = preloadQuery(GET_STATS, { + variables: { period: "month" }, + }); + + return { + userQueryRef, + statsQueryRef, + }; + }, +}); + +function RouteComponent() { + const { userQueryRef, statsQueryRef } = Route.useLoaderData(); + const { data: userData } = useReadQuery(userQueryRef); + const { data: statsData } = useReadQuery(statsQueryRef); + + return ( +
+

Welcome, {userData.user.name}

+
+

Monthly Stats

+

Views: {statsData.stats.views}

+

Clicks: {statsData.stats.clicks}

+
+
+ ); +} +``` + +### Using useQueryRefHandlers for Refetching + +When using `useReadQuery`, you can get refetch functionality from `useQueryRefHandlers`: + +> **Important:** Always call `useQueryRefHandlers` before `useReadQuery`. These two hooks interact with the same `queryRef`, and calling them in the wrong order could cause subtle bugs. + +```typescript +import { useReadQuery, useQueryRefHandlers, QueryRef } from "@apollo/client/react"; + +function UserComponent({ queryRef }: { queryRef: QueryRef }) { + const { refetch } = useQueryRefHandlers(queryRef); + const { data } = useReadQuery(queryRef); + + return ( +
+

{data.user.name}

+ +
+ ); +} +``` + +## Important Considerations + +1. **Import from Integration Package:** Always import `ApolloClient` and `InMemoryCache` from `@apollo/client-integration-tanstack-start`, not from `@apollo/client`, to ensure proper SSR hydration. + +2. **Context Type:** Use `createRootRouteWithContext()` to provide proper TypeScript types for the `preloadQuery` function in loaders. + +3. **Loader vs Component Queries:** + - Use `preloadQuery` in loaders when you want to start fetching data before the component renders + - Use `useSuspenseQuery` directly in components for simpler cases or when data fetching can wait until render + +4. **Streaming SSR:** The integration fully supports React's streaming SSR capabilities. Place `Suspense` boundaries strategically for optimal user experience. + +5. **Cache Management:** The Apollo Client instance is shared across all routes, so cache updates from one route will be reflected in all routes that use the same data. + +6. **Authentication:** Use Apollo Client's `SetContextLink` for dynamic auth tokens. + +## Advanced Configuration + +### Adding Authentication + +For authentication in TanStack Start with SSR support, you need to handle both server and client environments differently. Use `createIsomorphicFn` to provide environment-specific implementations: + +```typescript +import { ApolloClient, InMemoryCache, routerWithApolloClient } from "@apollo/client-integration-tanstack-start"; +import { ApolloLink, HttpLink } from "@apollo/client"; +import { SetContextLink } from "@apollo/client/link/context"; +import { createIsomorphicFn } from "@tanstack/react-start"; +import { createRouter } from "@tanstack/react-router"; +import { getSession, getCookie } from "@tanstack/react-start/server"; +import { routeTree } from "./routeTree.gen"; + +// Create isomorphic link that uses different implementations per environment +const createAuthLink = createIsomorphicFn() + .server(() => { + // Server-only: Can access server-side functions like `getCookies`, `getCookie`, `getSession`, etc. exported from `"@tanstack/react-start/server"` + return new SetContextLink(async (prevContext) => { + return { + headers: { + ...prevContext.headers, + authorization: getCookie("Authorization"), + }, + }; + }); + }) + .client(() => { + // Client-only: Can access `localStorage` or other browser APIs + return new SetContextLink((prevContext) => { + return { + headers: { + ...prevContext.headers, + authorization: localStorage.getItem("authToken") ?? "", + }, + }; + }); + }); + +export function getRouter() { + const httpLink = new HttpLink({ + uri: "https://your-graphql-endpoint.com/graphql", + }); + + const apolloClient = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.from([createAuthLink(), httpLink]), + }); + + const router = createRouter({ + routeTree, + context: { + ...routerWithApolloClient.defaultContext, + }, + }); + + return routerWithApolloClient(router, apolloClient); +} +``` + +> **Important:** The `getRouter` function is called both on the server and client, so it must not contain environment-specific code. Use `createIsomorphicFn` to provide different implementations: +> +> - **Server:** Can access server-only functions like `getSession`, `getCookies`, `getCookie` from `@tanstack/react-start/server` to access authentication information in request or session data +> - **Client:** Can use `localStorage` or other browser APIs to access auth tokens (if setting `credentials: "include"` is sufficient, try to prefer that over manually setting auth headers client-side) +> +> This ensures your authentication works correctly in both SSR and browser contexts. + +### Custom Cache Configuration + +```typescript +import { ApolloClient, InMemoryCache } from "@apollo/client-integration-tanstack-start"; +import { HttpLink } from "@apollo/client"; +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; +import { routerWithApolloClient } from "@apollo/client-integration-tanstack-start"; + +export function getRouter() { + const apolloClient = new ApolloClient({ + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + posts: { + merge(existing = [], incoming) { + return [...existing, ...incoming]; + }, + }, + }, + }, + }, + }), + link: new HttpLink({ uri: "https://your-graphql-endpoint.com/graphql" }), + }); + + const router = createRouter({ + routeTree, + context: { + ...routerWithApolloClient.defaultContext, + }, + }); + + return routerWithApolloClient(router, apolloClient); +} +``` diff --git a/.agents/skills/apollo-client/references/mutations.md b/.agents/skills/apollo-client/references/mutations.md new file mode 100644 index 000000000..8d9b65535 --- /dev/null +++ b/.agents/skills/apollo-client/references/mutations.md @@ -0,0 +1,540 @@ +# Mutations Reference + +## Table of Contents + +- [useMutation Hook](#usemutation-hook) +- [Mutation Variables](#mutation-variables) +- [Loading and Error States](#loading-and-error-states) +- [Optimistic UI](#optimistic-ui) +- [Cache Updates](#cache-updates) +- [Refetch Queries](#refetch-queries) +- [Error Handling](#error-handling) + +## useMutation Hook + +The `useMutation` hook is used to execute GraphQL mutations. + +### Basic Usage + +```tsx +import { gql } from "@apollo/client"; +import { useMutation } from "@apollo/client/react"; + +const ADD_TODO = gql` + mutation AddTodo($text: String!) { + addTodo(text: $text) { + id + text + completed + } + } +`; + +function AddTodo() { + const [addTodo, { data, loading, error }] = useMutation(ADD_TODO); + + return ( +
{ + e.preventDefault(); + const form = e.currentTarget; + const text = new FormData(form).get("text") as string; + addTodo({ variables: { text } }); + form.reset(); + }} + > + + + {error &&

Error: {error.message}

} +
+ ); +} +``` + +### Return Tuple + +```typescript +const [ + mutateFunction, // Function to call to execute mutation + { + data, // Mutation result data + loading, // True while mutation is in flight + error, // ApolloError if mutation failed + called, // True if mutation has been called + reset, // Reset mutation state + client, // Apollo Client instance + }, +] = useMutation(MUTATION); +``` + +## Mutation Variables + +### Variables in Options + +```tsx +const [createUser] = useMutation(CREATE_USER, { + variables: { + input: { + name: "Default User", + email: "default@example.com", + }, + }, +}); + +// Call with default variables +await createUser(); + +// Override variables +await createUser({ + variables: { + input: { + name: "Custom User", + email: "custom@example.com", + }, + }, +}); +``` + +### TypeScript Types + +Use `TypedDocumentNode` instead of generic type parameters: + +```typescript +import { gql, TypedDocumentNode } from "@apollo/client"; + +interface CreateUserData { + createUser: { + id: string; + name: string; + email: string; + }; +} + +interface CreateUserVariables { + input: { + name: string; + email: string; + }; +} + +const CREATE_USER: TypedDocumentNode = gql` + mutation CreateUser($input: CreateUserInput!) { + createUser(input: $input) { + id + name + email + } + } +`; + +const [createUser, { data, loading }] = useMutation(CREATE_USER); + +const { data } = await createUser({ + variables: { + input: { name: "John", email: "john@example.com" }, + }, +}); + +// data.createUser is fully typed +``` + +## Loading and Error States + +### Handling in UI + +```tsx +function CreatePost() { + const [createPost, { loading, error, data, reset }] = useMutation(CREATE_POST); + + if (data) { + return ( +
+

Post created: {data.createPost.title}

+ +
+ ); + } + + return ( +
+ +