diff --git a/.agents/analytics.md b/.agents/analytics.md deleted file mode 100644 index bcd12c824..000000000 --- a/.agents/analytics.md +++ /dev/null @@ -1,375 +0,0 @@ -# Analytics - -Reference for the TanStack.com event taxonomy in Google Analytics 4. - -**GA4 property:** `G-JMT1Z50SPS` -**First-party proxy:** all hits route through `/_a/g/collect` on tanstack.com (see `netlify.toml`) -**Implementation:** [src/utils/analytics.ts](../src/utils/analytics.ts), [src/utils/analytics/events.ts](../src/utils/analytics/events.ts) - -## Design principles - -1. **Event names describe what happened.** Properties carry context. Adding a new partner placement is an enum value, not a new event. -2. **One event per outcome, not per intent.** No `_clicked`/`_attempted` events when an outcome event is going to fire anyway. -3. **No per-row events.** Counts and joined-string arrays as properties, not N rows per generation. -4. **Session context propagates.** Slow-changing props like `mode_used` and `idea_used` are stamped on every builder event so any breakdown works without joins. -5. **Typed registry.** Wrong props for an event = TypeScript error, not silent bad data. - ---- - -## The funnel - -Application Builder, four steps. No `OR` conditions, no overlapping event names. - -| # | Step | Event | Filter | -| --- | --------------------- | ------------------- | --------------------------------- | -| 1 | Landed on builder | `page_view` | `page_type = application_builder` | -| 2 | Got an analysis | `builder_analyzed` | — | -| 3 | Got a generation | `builder_generated` | — | -| 4 | Took action on result | `builder_activated` | — | - -Drop-off between any two steps is unambiguous. - -**Failure rates** are separate explorations, not branches off the funnel: - -- Analysis failure rate = `builder_failed[stage=analysis]` ÷ (`builder_analyzed` + `builder_failed[stage=analysis]`) -- Generation failure rate = `builder_failed[stage=generation]` ÷ (`builder_generated` + `builder_failed[stage=generation]`) -- Login wall rate = `builder_failed[stage=login_blocked]` ÷ `page_view[page_type=application_builder]` - ---- - -## Event reference (9 events) - -Every event automatically receives `page_location`, `page_path`, `page_title`, `page_type` from the analytics utility. - -### `page_view` - -Fires on initial load (auto from gtag config) and on every SPA navigation. - -| Prop | Type | Notes | -| --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | -| `page_location` | string | Full URL | -| `page_path` | string | Pathname | -| `page_title` | string | `document.title` | -| `page_type` | enum | `home`, `partners_index`, `partner_detail`, `blog_index`, `blog_post`, `docs`, `partners_embed`, `application_builder`, `page` | - ---- - -### `partner_viewed` - -A partner UI element scrolled ≥50% into the viewport. Fires once per element-mount per session (the underlying `IntersectionObserver` disconnects after first fire). - -| Prop | Type | Notes | -| ------------ | ------- | --------------------------------------------------- | -| `partner_id` | string | Stable partner identifier | -| `placement` | enum | See `PartnerPlacement` below | -| `slot_index` | number? | Position in the surface (0-indexed) when applicable | - -**Note on inflation:** when filters change on the partners directory, cards unmount/remount and `partner_viewed` re-fires. Treat session-unique impressions as the dedup'd metric (compute in BigQuery with `FIRST_VALUE(... PARTITION BY session_id, partner_id)`). - ---- - -### `partner_clicked` - -User clicked a partner UI element to navigate somewhere. - -| Prop | Type | Notes | -| ------------------ | ------- | -------------------------------------------------------------------------- | -| `partner_id` | string | | -| `placement` | enum | See `PartnerPlacement` below | -| `destination` | enum | `external` (partner's site) or `internal_detail` (our partner detail page) | -| `destination_host` | string? | Host of the destination URL when `external` | -| `slot_index` | number? | Position in the surface when applicable | - -CTR per placement = `partner_clicked` ÷ `partner_viewed` filtered to same `placement`. - ---- - -### `partner_filter_applied` - -User changed the filter state on the partners directory. - -| Prop | Type | Notes | -| ----------------- | -------------- | --------------------------------------------------------------------------------------------- | -| `change` | enum | `libraries_changed`, `status_changed`, `cleared_all` | -| `library_filters` | string | Comma-joined library ids in the filter, or empty string | -| `status_filter` | string \| null | `null` means "no status filter applied" — distinguishes from "user explicitly chose 'active'" | -| `result_count` | number | Number of partners visible after the filter | - ---- - -### `partner_inquiry_started` - -User clicked a "get in touch", "let's chat", or "become a partner" CTA. - -| Prop | Type | Notes | -| ----------- | ---- | ---------------------------------------------------------- | -| `placement` | enum | `partners_index_cta`, `library_callout`, `docs_right_rail` | - ---- - -### `builder_analyzed` - -Analysis API call succeeded. Outcome event — always preceded by user intent, no separate `_requested` event. - -| Prop | Type | Notes | -| ------------------------ | ------- | --------------------------------------------- | -| `mode_used` | enum | Session context: `lucky`, `confident`, `none` | -| `idea_used` | string | Session context: idea label, or `none` | -| `analysis_deployment` | string? | Inferred deploy target | -| `inferred_library_count` | number | | -| `inferred_partner_count` | number | | -| `feature_count` | number | | - ---- - -### `builder_generated` - -Generation API call succeeded. - -| Prop | Type | Notes | -| ----------------------- | ------- | ---------------------------------------------------------------------- | -| `mode_used` | enum | Session context | -| `idea_used` | string | Session context | -| `final_deployment` | string? | The chosen deploy target on the final result | -| `final_package_manager` | string | `pnpm`, `npm`, `yarn`, `bun` | -| `final_library_count` | number | | -| `final_partner_count` | number | | -| `final_addon_count` | number | | -| `library_ids` | string | Comma-joined LibraryIds — use `SPLIT()` in BigQuery for top-N analysis | -| `partner_ids` | string | Comma-joined | -| `addon_ids` | string | Comma-joined | - ---- - -### `builder_failed` - -Single umbrella event for analysis failures, generation failures, and login-wall blocks. Use the `stage` prop to distinguish. - -| Prop | Type | Notes | -| --------------------------------- | ------- | -------------------------------------------------------------------------------------------- | -| `mode_used` | enum | Session context | -| `idea_used` | string | Session context | -| `stage` | enum | `analysis`, `generation`, `login_blocked` | -| `error_message` | string? | Free-form error message — high cardinality, don't register as a dimension; query in BigQuery | -| `retry_after` | number? | Seconds until retry permitted (login_blocked only) | -| `anonymous_generations_remaining` | number? | When the failure was rate-limit related | - ---- - -### `builder_activated` - -User took an action on the generated result. Single event with `action` prop covers all post-generation actions. - -| Prop | Type | Notes | -| ----------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `mode_used` | enum | Session context | -| `idea_used` | string | Session context | -| `action` | enum | See `BuilderAction` below | -| `surface` | enum | `result_panel` (main builder UI) or `deploy_dialog` | -| `provider` | string? | Deploy provider when applicable: `vercel`, `netlify`, `cloudflare` | -| `automatic` | boolean | `true` for system-driven actions (e.g., deploy_dialog auto-redirect countdown). Filter to `false` for true user click rates. | - -**Important:** automatic prompt-copies that fire as a side-effect of generation do NOT emit `builder_activated`. Only user-driven actions count as activation. - ---- - -## Enums - -### `PartnerPlacement` - -| Value | Where it appears | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -| `directory` | Partner cards in `/partners` index | -| `detail` | Partner detail page CTA | -| `docs_rail` | Right rail on docs pages — partner cards AND the "Become a Partner" link both fire with this placement (event name distinguishes) | -| `blog_rail` | Right rail on blog pages | -| `grid` | Generic partners grid (fallback) | -| `home_grid` | Home page social-proof grid | -| `library_grid` | Library page partners section — filter `page_path` to know which library | -| `embed_grid` | Partners embed view | -| `docs_strip` | Mobile partner strip in docs | -| `ecosystem_game` | 3D ecosystem game islands | -| `partners_index_cta` | "Get in touch" mailto on `/partners` | -| `library_callout` | "Let's chat" callout per library | - -### `BuilderAction` - -| Value | Means | -| -------------------------- | ---------------------------------------------------------------------- | -| `copy_prompt` | User clicked a copy button on the prompt | -| `deploy` | Started a deploy through the deploy dialog | -| `clone_repo` | Cloned the GitHub repo | -| `open_codex` | Opened the result in Codex | -| `open_claude` | Opened the result in Claude | -| `open_cursor` | Opened the result in Cursor | -| `download` | Downloaded the project as a zip | -| `open_advanced` | Opened the advanced builder editor | -| `netlify_start` | Started a Netlify deploy from the result | -| `provider_redirect_manual` | User clicked through to deploy provider | -| `provider_redirect_auto` | Countdown auto-redirected user to deploy provider (`automatic = true`) | -| `open_repo` | Opened the project repo from the deploy dialog | - ---- - -## Session context - -`mode_used` and `idea_used` are tracked in the builder hook and stamped on **every** builder event. This means any builder event can be sliced by mode or idea without session joins. - -**`mode_used`** transitions: - -- `none` → `lucky` when user clicks "I'm feeling lucky" -- `none` → `confident` when user clicks "I'm feeling confident" - -**`idea_used`** transitions: - -- `none` → idea label string when user picks a suggested idea -- Reset back to `none` if user clears or types fresh input (TBD — currently sticks for the session) - ---- - -## Custom dimensions to register in GA4 - -Admin → Custom definitions → Create custom dimension. **Event scope** for all of these. Without registration, they're stored in BigQuery export but invisible in the GA4 UI. - -| Dimension name | API name | Used on | -| --------------------- | ----------------------- | -------------------------------------------------------- | -| Placement | `placement` | partner_viewed, partner_clicked, partner_inquiry_started | -| Partner ID | `partner_id` | partner_viewed, partner_clicked | -| Mode used | `mode_used` | all builder events | -| Idea used | `idea_used` | all builder events | -| Action | `action` | builder_activated | -| Surface | `surface` | builder_activated | -| Stage | `stage` | builder_failed | -| Final deployment | `final_deployment` | builder_generated | -| Final package manager | `final_package_manager` | builder_generated | -| Final library count | `final_library_count` | builder_generated | -| Final partner count | `final_partner_count` | builder_generated | -| Page type | `page_type` | all events | - -12 dimensions. Well under the 50-dimension event-scoped limit. - -**Don't register:** `error_message`, `library_ids`, `partner_ids`, `addon_ids`, `destination_host`. High cardinality. Query in BigQuery. - ---- - -## Common GA4 queries - -### "What's our funnel completion rate?" - -**Explore → Funnel exploration**, four steps as defined above. Open funnel. Show elapsed time. - -### "Lucky vs Confident: which mode converts better?" - -Same funnel, breakdown dropdown = `mode_used`. Compare side by side. - -### "Which deploy targets do successful generations land on?" - -**Explore → Free-form**, dimension = `final_deployment`, metric = event count of `builder_generated`. - -### "What % of users hit the login wall?" - -Free-form, filter `event_name = builder_failed`, breakdown by `stage`. - -### "Which placement converts best for partner discovery?" - -Free-form, filter `event_name = partner_clicked OR partner_viewed`, breakdown by `placement`. Compute CTR yourself: clicks ÷ views per placement. - -### "Top 10 libraries in completed generations" - -Requires BigQuery — `library_ids` is high-cardinality. Sample query: - -```sql -SELECT - library_id, - COUNT(*) AS generations -FROM `tanstack.analytics_*.events_*`, -UNNEST(SPLIT((SELECT value.string_value - FROM UNNEST(event_params) - WHERE key = 'library_ids'), ',')) AS library_id -WHERE event_name = 'builder_generated' - AND _TABLE_SUFFIX BETWEEN '20260101' AND '20260131' -GROUP BY library_id -ORDER BY generations DESC -LIMIT 10 -``` - ---- - -## BigQuery export - -**Strongly recommended.** Free at our event volume. Without it, anything beyond stock reports is painful. - -Setup: - -1. GA4 Admin → BigQuery Links → Link -2. Pick a GCP project, daily export, US multi-region -3. Tables appear at `tanstack.analytics_.events_YYYYMMDD` after ~24h - -Once enabled, all event properties are queryable — including the ones not registered as custom dimensions. Use SQL for everything dimensional or aggregate-heavy. Use the GA4 UI for funnel exploration and headline numbers. - ---- - -## Adding new events - -Anything additive should not require schema migration of the existing taxonomy. - -**Add a new partner placement:** - -1. Add the value to `PartnerPlacement` in [src/utils/analytics/events.ts](../src/utils/analytics/events.ts) -2. Use it at the call site -3. (Optional) update the placement table in this doc - -**Add a new builder action:** - -1. Add the value to `BuilderAction` -2. Use it at the `builder_activated` call site - -**Add a new event:** - -1. Add a new union member to `AnalyticsEvent` with its prop interface -2. Call `trackEvent({ name: '...', props: { ... } })` -3. Register relevant breakdown props as custom dimensions in GA4 admin -4. Document the event in this file - -**Don't:** add new properties to existing events without updating both the type definition and this doc. Schema drift in analytics events is the slowest bug to detect. - ---- - -## Code locations - -| File | Purpose | -| ------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -| [src/utils/analytics.ts](../src/utils/analytics.ts) | `trackEvent`, `useTrackedImpression`, `trackPageView`, `getPageType` | -| [src/utils/analytics/events.ts](../src/utils/analytics/events.ts) | Typed event registry — discriminated union of all events | -| [src/utils/analytics/providers/google.ts](../src/utils/analytics/providers/google.ts) | gtag wrapper | -| [src/utils/analytics/types.ts](../src/utils/analytics/types.ts) | Provider interface | -| [src/routes/\_\_root.tsx](../src/routes/__root.tsx) | gtag bootstrap, `PageViewTracker` | -| [netlify.toml](../netlify.toml) | First-party proxy redirects | - ---- - -## Migration history - -### 2026-05 — schema v2 - -Collapsed 28 events into 9. Removed `application_starter_*` event family. Replaced with `builder_*` events. Mode and idea selection moved from standalone events into session-context props on every builder event. - -**Events removed entirely** (no replacement): - -- `application_starter_library_toggled`, `_integration_toggled`, `_package_manager_toggled`, `_toolchain_toggled` — config exploration depth no longer tracked. Final config is on `builder_generated`. -- `application_starter_continue_clicked`, `_generate_clicked` — intent implied by outcome events. -- `application_starter_login_clicked`, `_value_copied` (auto), `_builder_result_applied`, `_final_partner_in_prompt`, `_final_addon_in_prompt`. - -**Events folded into others:** - -- `application_starter_action_clicked` (mode_selected) → `mode_used` prop on subsequent events -- `application_starter_idea_selected` → `idea_used` prop on subsequent events -- `application_starter_login_required` → `builder_failed[stage=login_blocked]` -- `application_starter_value_copied` (user trigger) → `builder_activated[action=copy_prompt]` -- All other `application_starter_action_clicked` calls → `builder_activated` -- `partner_card_clicked`, `partner_click` → `partner_clicked` -- `partner_impression`, `partner_detail_viewed` → `partner_viewed` -- `partners_filter_changed` → `partner_filter_applied` -- `partner_inquiry_clicked`, `become_partner_clicked` → `partner_inquiry_started` - -Historical data with old event names is still queryable in GA4 and BigQuery. Cutover date: see git log for the migration commit. diff --git a/.agents/index.md b/.agents/index.md deleted file mode 100644 index 01316aeb9..000000000 --- a/.agents/index.md +++ /dev/null @@ -1,19 +0,0 @@ -# Agent Guidelines - -TanStack.com marketing site built with TanStack Start. - -## Essentials - -- Package manager: `pnpm` -- Run `pnpm test` before commits not after every tiny edit -- Don't run builds or tests after every change. This is a visual site; assume changes work unless reported otherwise. -- Rely on `tsc` or your built-in LSP integration (hopefully you have this) -- **Typesafety is paramount.** Never cast types; fix at source instead. See [typescript.md](./typescript.md). - -## Topic Guides - -- [TypeScript Conventions](./typescript.md): Type inference, casting rules, generic naming -- [TanStack Patterns](./tanstack-patterns.md): Loaders, server functions, environment shaking -- [UI Style Guide](./ui-style.md): Visual design principles for 2026 -- [Workflow](./workflow.md): Build commands, debugging, Playwright -- [Analytics](./analytics.md): GA4 event taxonomy, funnel definition, custom dimensions diff --git a/.agents/tanstack-patterns.md b/.agents/tanstack-patterns.md deleted file mode 100644 index d0ff4e278..000000000 --- a/.agents/tanstack-patterns.md +++ /dev/null @@ -1,76 +0,0 @@ -# TanStack Patterns - -## loaderDeps Must Be Specific - -Only include properties actually used in the loader. This ensures proper cache invalidation. - -```typescript -// Bad: includes everything -loaderDeps: ({ search }) => search, -loader: async ({ deps }) => { - await fetchData({ page: deps.page, pageSize: deps.pageSize }) -} - -// Good: only what's used -loaderDeps: ({ search }) => ({ - page: search.page, - pageSize: search.pageSize, -}), -loader: async ({ deps }) => { - await fetchData({ page: deps.page, pageSize: deps.pageSize }) -} -``` - -## Loaders Are Isomorphic - -Loaders run on both server and client. They cannot directly access server-only APIs. - -```typescript -// Bad: direct server API access -loader: async () => { - const data = await fs.readFile('data.json') - return { data } -} - -// Good: call a server function -loader: async () => { - const data = await serverFn({ data: { id: '123' } }) - return { data } -} -``` - -## Environment Shaking - -TanStack Start strips any code not referenced by a `createServerFn` handler from the client build. - -- Server-only code (database, fs) is automatically excluded from client bundles -- Only code inside `createServerFn` handlers goes to server bundles -- Code outside handlers is included in both bundles - -## Importing Server Functions - -Server functions wrapped in `createServerFn` can be imported statically. Never use dynamic imports for server-only code in components. - -```typescript -// Bad: dynamic import causes bundler issues -const rolesQuery = useQuery({ - queryFn: async () => { - const { listRoles } = await import('~/utils/roles.server') - return listRoles({ data: {} }) - }, -}) - -// Good: static import -import { listRoles } from '~/utils/roles.server' - -const rolesQuery = useQuery({ - queryFn: async () => listRoles({ data: {} }), -}) -``` - -## Server-Only Import Rules - -1. `createServerFn` wrappers can be imported statically anywhere -2. Direct server-only code (database clients, fs) must only be imported: - - Inside `createServerFn` handlers - - In `*.server.ts` files diff --git a/.agents/typescript.md b/.agents/typescript.md deleted file mode 100644 index e63301ec4..000000000 --- a/.agents/typescript.md +++ /dev/null @@ -1,45 +0,0 @@ -# TypeScript Conventions - -## Avoid Type Casting - -Never cast types unless absolutely necessary. This includes: - -- Manual generic type parameters (e.g., ``) -- Type assertions using `as` -- Type assertions using `satisfies` - -## Prefer Type Inference - -Infer types by going up the logical chain: - -1. **Schema validation** as source of truth (Convex, Zod, etc.) -2. **Type inference** from function return types, API responses -3. **Fix at source** (schema, API definition, function signature) rather than casting at point of use - -```typescript -// Bad -const result = api.getData() as MyType -const value = getValue() - -// Good -const result = api.getData() // Type inferred from return type -const value = getValue() // Type inferred from implementation -``` - -## Generic Type Parameter Naming - -All generic type parameters must be prefixed with `T`. - -```typescript -// Bad -function withCapability( - handler: (user: AuthUser, ...args: Args) => R, -) { ... } - -// Good -function withCapability( - handler: (user: AuthUser, ...args: TArgs) => TReturn, -) { ... } -``` - -Common names: `T`, `TArgs`, `TReturn`, `TData`, `TError`, `TKey`, `TValue` diff --git a/.agents/ui-style.md b/.agents/ui-style.md deleted file mode 100644 index d696b3346..000000000 --- a/.agents/ui-style.md +++ /dev/null @@ -1,52 +0,0 @@ -# UI Style Guide 2026 - -## Layout - -- Fewer, well-defined containers over many small sections -- Generous spacing creates separation before adding visual effects -- Cards are acceptable when they express grouping or hierarchy - -## Corners - -- Rounded corners are standard -- Subtle radius values that feel intentional, not playful -- Avoid sharp 90-degree corners unless intentionally industrial - -## Shadows and Depth - -- Soft, low-contrast, diffused shadows -- Shadows imply separation, not elevation theatrics -- No heavy drop shadows or strong directional lighting -- One to two shadow layers max - -## Cards - -- Cards should feel grounded, not floating -- Light elevation, border plus shadow, or surface contrast -- Don't overuse cards as a default layout primitive - -## Color and Surfaces - -- Soft neutrals, off-whites, warm grays -- Surface contrast or translucency instead of strong outlines -- Glass/frosted effects acceptable when subtle and accessible - -## Interaction - -- Micro transitions reinforce spatial relationships -- Hover/focus states feel responsive, not animated -- No excessive motion or springy effects - -## Typography - -- Strong headings, calm body text -- No visual noise around content - -## What to Avoid - -- Chunky shadows -- Overly flat, sterile layouts -- Neumorphism as primary style -- Over-designed card grids - -**Summary: If depth does not improve comprehension, remove it.** diff --git a/.agents/workflow.md b/.agents/workflow.md deleted file mode 100644 index 07838ff50..000000000 --- a/.agents/workflow.md +++ /dev/null @@ -1,46 +0,0 @@ -# Workflow - -## Build Commands - -- `pnpm test`: Run at end of task batches -- `pnpm build`: Only for build/bundler issues or verifying production output -- `pnpm lint`: Check for code issues -- `dev` runs indefinitely in watch mode - -Don't build after every change. This is a visual site; assume changes work. - -## Dev Authentication - -The dev server uses the real production database and OAuth. There are no auth bypasses. - -To authenticate a dev session, the human must run: - -```sh -pnpm auth:login -``` - -This opens `tanstack.com`, completes OAuth, and saves `DEV_SESSION_TOKEN` to `.env.local`. The dev server then auto-injects that token as a session cookie on startup. - -If the user asks for help with anything requiring authentication (account pages, admin, mutations) and `DEV_SESSION_TOKEN` is not set in `.env.local`, tell them to run `pnpm auth:login` first and restart the dev server. - -## Debugging Visual Issues - -When something doesn't work or look right: - -1. Use Playwright MCP to view the page and debug visually -2. Use `pnpm build` only for build/bundler issues -3. Use `pnpm lint` for code issues - -## Playwright - -Playwright is available via `npx playwright` and should be used freely to interact with the running dev server like a human would. This includes testing, debugging, visual verification, design iteration, and general development workflows. - -Since the dev server runs with a real authenticated session (via `DEV_SESSION_TOKEN`), Playwright has full access to gated pages, account features, admin areas, and authenticated mutations — exactly as the signed-in user would. - -Common uses: - -- `npx playwright screenshot ` — capture current state of any page -- Navigate to a page, interact with elements, re-screenshot to verify changes -- Debug layout/visual issues without needing a human to look -- Verify authenticated flows end-to-end (account, showcase submissions, admin, etc.) -- Iterate on UI by screenshotting, editing, screenshotting again diff --git a/.claude/launch.json b/.claude/launch.json deleted file mode 100644 index 7e34cf155..000000000 --- a/.claude/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "0.0.1", - "configurations": [ - { - "name": "web", - "runtimeExecutable": "pnpm", - "runtimeArgs": ["dev"], - "port": 3000 - }, - { - "name": "db-studio", - "runtimeExecutable": "pnpm", - "runtimeArgs": ["db:studio"], - "port": 4983 - } - ] -} diff --git a/.claude/tanstack-patterns.md b/.claude/tanstack-patterns.md deleted file mode 120000 index 7335c6c22..000000000 --- a/.claude/tanstack-patterns.md +++ /dev/null @@ -1 +0,0 @@ -../.agents/tanstack-patterns.md \ No newline at end of file diff --git a/.claude/typescript.md b/.claude/typescript.md deleted file mode 120000 index bf4e25139..000000000 --- a/.claude/typescript.md +++ /dev/null @@ -1 +0,0 @@ -../.agents/typescript.md \ No newline at end of file diff --git a/.claude/ui-style.md b/.claude/ui-style.md deleted file mode 120000 index f59b7412f..000000000 --- a/.claude/ui-style.md +++ /dev/null @@ -1 +0,0 @@ -../.agents/ui-style.md \ No newline at end of file diff --git a/.claude/workflow.md b/.claude/workflow.md deleted file mode 120000 index c19e63b75..000000000 --- a/.claude/workflow.md +++ /dev/null @@ -1 +0,0 @@ -../.agents/workflow.md \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..af7fa28f8 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,5 @@ +{ + "extends": ["react-app"], + "parser": "@typescript-eslint/parser", + "plugins": ["react-hooks"] +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index fd99b3b17..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: tannerlinsley diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml deleted file mode 100644 index b336e672a..000000000 --- a/.github/workflows/autofix.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: autofix.ci # needed to securely identify the workflow - -on: - pull_request: - push: - branches: [main] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.number || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - autofix: - name: autofix - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - name: Setup Tools - uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - - name: Fix formatting - run: pnpm format - - name: Apply fixes - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 - with: - commit-message: 'ci: apply automated fixes' diff --git a/.github/workflows/docs-links.yml b/.github/workflows/docs-links.yml deleted file mode 100644 index 4771fdd25..000000000 --- a/.github/workflows/docs-links.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Docs Links - -on: - workflow_dispatch: - schedule: - - cron: '0 9 * * *' - -permissions: - contents: read - -jobs: - docs-links: - name: Docs Links - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Setup Tools - uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - - - name: Check docs menu links - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - set +e - pnpm docs:links:check -- --json docs-menu-link-report.json - status=$? - - if [ -f docs-menu-link-report.json ]; then - { - echo "### Docs menu link report" - echo - echo '```json' - cat docs-menu-link-report.json - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - fi - - exit $status diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 000000000..0a534520b --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,25 @@ +name: PR + +on: + pull_request: + +jobs: + pr: + name: PR + runs-on: ubuntu-latest + steps: + - name: Git Checkout + uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: pnpm + - name: Install Packages + run: pnpm install --frozen-lockfile + - name: Run Lint + run: pnpm lint + - name: Run Build + run: pnpm build diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml deleted file mode 100644 index d4497f807..000000000 --- a/.github/workflows/pr.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: PR - -on: - pull_request: - -permissions: - contents: read - -jobs: - pr: - name: PR - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - name: Setup Tools - uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - - name: Run Build - run: pnpm build - - name: Run Tests - run: pnpm test diff --git a/.github/workflows/update-tanstack-deps.yml b/.github/workflows/update-tanstack-deps.yml deleted file mode 100644 index 7d4ad61d9..000000000 --- a/.github/workflows/update-tanstack-deps.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Update TanStack Dependencies - -on: - workflow_dispatch: - schedule: - # Run weekly on Sundays at 10:00 AM UTC - - cron: '0 10 * * 0' - -jobs: - update-deps: - name: Update TanStack Dependencies - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Git Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - # This scheduled job commits dependency updates back to the branch. - persist-credentials: true - - - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 - - - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: .nvmrc - cache: pnpm - - - name: Install Packages - run: pnpm install --frozen-lockfile - - - name: Update TanStack Dependencies - run: pnpm up "@tanstack/*" --latest - - - name: Check for Changes - id: git-check - run: | - git status --porcelain . - if [[ -z $(git status --porcelain .) ]]; then - echo "No changes detected" - echo "changes=false" >> $GITHUB_OUTPUT - else - echo "Changes detected" - echo "changes=true" >> $GITHUB_OUTPUT - fi - - - name: Run Lint - if: steps.git-check.outputs.changes == 'true' - run: pnpm lint - - - name: Run Build - if: steps.git-check.outputs.changes == 'true' - run: pnpm build - - - name: Commit and Push Changes - if: steps.git-check.outputs.changes == 'true' - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add . - git commit -m "chore: update @tanstack/* dependencies" - git push diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml deleted file mode 100644 index 1d4088db8..000000000 --- a/.github/workflows/zizmor.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: GitHub Actions Security Analysis - -on: - push: - branches: [main] - pull_request: - branches: ['**'] - -permissions: {} - -jobs: - zizmor: - name: Run zizmor - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Run zizmor - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 - with: - advanced-security: false - annotations: true diff --git a/.gitignore b/.gitignore index 7021fe7e7..851c25acf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,14 @@ node_modules package-lock.json yarn.lock -drizzle/migrations -.tanstack .DS_Store .cache .env -.env.* .vercel .output .vinxi -.tanstack-start/build -.nitro/* -.netlify/* -.wrangler/* +.netlify /build/ /api/ @@ -30,10 +24,3 @@ dist # Content Collections generated files .content-collections -test-results -.claude/CLAUDE.md -.claude/worktrees -.eslintcache -.tsbuildinfo -src/routeTree.gen.ts -.og-preview/ diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index 63860b5bd..000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -pnpm husky diff --git a/.nvmrc b/.nvmrc index d9b042470..c12134be3 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v25.9.0 +v20.15.0 diff --git a/.oxfmtrc.json b/.oxfmtrc.json deleted file mode 100644 index ac154e249..000000000 --- a/.oxfmtrc.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "./node_modules/oxfmt/configuration_schema.json", - "semi": false, - "singleQuote": true, - "trailingComma": "all", - "printWidth": 80, - "sortPackageJson": false, - "ignorePatterns": [ - "**/api", - "**/build", - "**/public", - "pnpm-lock.yaml", - "routeTree.gen.ts", - "src/blog/tanstack-db-0.1-the-embedded-client-database-for-tanstack-query.md", - ".content-collections", - ".claude", - "dist/**", - ".output/**" - ] -} diff --git a/.oxlintrc.json b/.oxlintrc.json deleted file mode 100644 index f6467d88c..000000000 --- a/.oxlintrc.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": ["typescript"], - "categories": { - "correctness": "off" - }, - "options": { - "typeAware": true, - "typeCheck": true - }, - "env": { - "builtin": true - }, - "ignorePatterns": [ - "node_modules", - "dist", - "build", - ".content-collections", - ".tanstack-start", - ".netlify", - "public", - "convex/.temp", - ".claude" - ], - "rules": { - "no-array-constructor": "error", - "no-unused-expressions": "error", - "no-unused-vars": "error", - "typescript/ban-ts-comment": "error", - "typescript/no-duplicate-enum-values": "error", - "typescript/no-empty-object-type": "error", - "typescript/no-explicit-any": "error", - "typescript/no-extra-non-null-assertion": "error", - "typescript/no-misused-new": "error", - "typescript/no-namespace": "error", - "typescript/no-non-null-asserted-optional-chain": "error", - "typescript/no-require-imports": "error", - "typescript/no-this-alias": "error", - "typescript/no-unnecessary-type-constraint": "error", - "typescript/no-unsafe-declaration-merging": "error", - "typescript/no-unsafe-function-type": "error", - "typescript/no-wrapper-object-types": "error", - "typescript/prefer-as-const": "error", - "typescript/prefer-namespace-keyword": "error", - "typescript/triple-slash-reference": "error" - }, - "overrides": [ - { - "files": ["**/*.{js,jsx}"], - "rules": { - "constructor-super": "error", - "for-direction": "error", - "no-async-promise-executor": "error", - "no-case-declarations": "error", - "no-class-assign": "error", - "no-compare-neg-zero": "error", - "no-cond-assign": "error", - "no-const-assign": "error", - "no-constant-binary-expression": "error", - "no-constant-condition": "error", - "no-control-regex": "error", - "no-debugger": "error", - "no-delete-var": "error", - "no-dupe-class-members": "error", - "no-dupe-else-if": "error", - "no-dupe-keys": "error", - "no-duplicate-case": "error", - "no-empty": "error", - "no-empty-character-class": "error", - "no-empty-pattern": "error", - "no-empty-static-block": "error", - "no-ex-assign": "error", - "no-extra-boolean-cast": "error", - "no-fallthrough": "error", - "no-func-assign": "error", - "no-global-assign": "error", - "no-import-assign": "error", - "no-invalid-regexp": "error", - "no-irregular-whitespace": "error", - "no-loss-of-precision": "error", - "no-misleading-character-class": "error", - "no-new-native-nonconstructor": "error", - "no-nonoctal-decimal-escape": "error", - "no-obj-calls": "error", - "no-prototype-builtins": "error", - "no-redeclare": "error", - "no-regex-spaces": "error", - "no-self-assign": "error", - "no-setter-return": "error", - "no-shadow-restricted-names": "error", - "no-sparse-arrays": "error", - "no-this-before-super": "error", - "no-unexpected-multiline": "error", - "no-unsafe-finally": "error", - "no-unsafe-negation": "error", - "no-unsafe-optional-chaining": "error", - "no-unused-labels": "error", - "no-unused-private-class-members": "error", - "no-useless-backreference": "error", - "no-useless-catch": "error", - "no-useless-escape": "error", - "no-with": "error", - "require-yield": "error", - "use-isnan": "error", - "valid-typeof": "error" - } - }, - { - "files": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"], - "rules": { - "constructor-super": "off", - "no-class-assign": "off", - "no-const-assign": "off", - "no-dupe-class-members": "off", - "no-dupe-keys": "off", - "no-func-assign": "off", - "no-import-assign": "off", - "no-new-native-nonconstructor": "off", - "no-obj-calls": "off", - "no-redeclare": "off", - "no-setter-return": "off", - "no-this-before-super": "off", - "no-unsafe-negation": "off", - "no-var": "error", - "no-with": "off", - "prefer-const": "error", - "prefer-rest-params": "error", - "prefer-spread": "error" - } - }, - { - "files": ["**/*.{ts,tsx}"], - "rules": { - "no-redeclare": "error", - "no-shadow": "off", - "no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "(^_)|(^__+$)|(^e$)|(^error$)", - "varsIgnorePattern": "(^_)|(^__+$)|(^e$)|(^error$)", - "caughtErrorsIgnorePattern": "(^_)|(^__+$)|(^e$)|(^error$)" - } - ], - "typescript/no-explicit-any": "off" - } - }, - { - "files": ["**/*.{ts,tsx,js,jsx}"], - "rules": { - "jsx-a11y/alt-text": "error", - "jsx-a11y/anchor-ambiguous-text": "off", - "jsx-a11y/anchor-has-content": "error", - "jsx-a11y/anchor-is-valid": "error", - "jsx-a11y/aria-activedescendant-has-tabindex": "error", - "jsx-a11y/aria-props": "error", - "jsx-a11y/aria-proptypes": "error", - "jsx-a11y/aria-role": "error", - "jsx-a11y/aria-unsupported-elements": "error", - "jsx-a11y/autocomplete-valid": "error", - "jsx-a11y/click-events-have-key-events": "error", - "jsx-a11y/heading-has-content": "error", - "jsx-a11y/html-has-lang": "error", - "jsx-a11y/iframe-has-title": "error", - "jsx-a11y/img-redundant-alt": "error", - "jsx-a11y/label-has-associated-control": "error", - "jsx-a11y/media-has-caption": "error", - "jsx-a11y/mouse-events-have-key-events": "error", - "jsx-a11y/no-access-key": "error", - "jsx-a11y/no-autofocus": "error", - "jsx-a11y/no-distracting-elements": "error", - "jsx-a11y/no-noninteractive-tabindex": [ - "error", - { - "tags": [], - "roles": ["tabpanel"], - "allowExpressionValues": true - } - ], - "jsx-a11y/no-redundant-roles": "error", - "jsx-a11y/no-static-element-interactions": [ - "error", - { - "allowExpressionValues": true, - "handlers": [ - "onClick", - "onMouseDown", - "onMouseUp", - "onKeyPress", - "onKeyDown", - "onKeyUp" - ] - } - ], - "jsx-a11y/role-has-required-aria-props": "error", - "jsx-a11y/role-supports-aria-props": "error", - "jsx-a11y/scope": "error", - "jsx-a11y/tabindex-no-positive": "error", - "react/rules-of-hooks": "error", - "react/exhaustive-deps": "warn" - }, - "plugins": ["react", "jsx-a11y"] - } - ] -} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..b40caf3a5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +**/api +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts +convex/_generated +convex/README.md \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..fd496a820 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "semi": false +} diff --git a/AGENTS.md b/AGENTS.md deleted file mode 120000 index 49055565e..000000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -.agents/index.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 49055565e..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -.agents/index.md \ No newline at end of file diff --git a/README.md b/README.md index f168f491e..90cba4aac 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,15 @@ -
+# Welcome to TanStack.com! -# TanStack.com +This site is built with TanStack Router! -The home of the TanStack ecosystem. Built with [TanStack Router](https://tanstack.com/router) and deployed on [Cloudflare Workers](https://workers.cloudflare.com/). +- [TanStack Router Docs](https://tanstack.com/router) -Follow @TanStack +It's deployed automagically with Netlify! -### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/) - -
+- [Netlify](https://netlify.com/) ## Development -### Quick Start - From your terminal: ```sh @@ -23,64 +19,54 @@ pnpm dev This starts your app in development mode, rebuilding assets on file changes. -### Local Setup +## Editing and previewing the docs of TanStack projects locally + +The documentations for all TanStack projects except for `React Charts` are hosted on [https://tanstack.com](https://tanstack.com), powered by this TanStack Router app. +In production, the markdown doc pages are fetched from the GitHub repos of the projects, but in development they are read from the local file system. -The documentation for all TanStack projects (except `React Charts`) is hosted on [tanstack.com](https://tanstack.com). In production, doc pages are fetched from GitHub. In development, they're read from your local file system. +Follow these steps if you want to edit the doc pages of a project (in these steps we'll assume it's [`TanStack/form`](https://github.com/tanstack/form)) and preview them locally : -Create a `tanstack` parent directory and clone this repo alongside the projects: +1. Create a new directory called `tanstack`. ```sh -mkdir tanstack && cd tanstack -git clone git@github.com:TanStack/tanstack.com.git -git clone git@github.com:TanStack/query.git -git clone git@github.com:TanStack/router.git -git clone git@github.com:TanStack/table.git +mkdir tanstack ``` -Your directory structure should look like this: +2. Enter the directory and clone this repo and the repo of the project there. +```sh +cd tanstack +git clone git@github.com:TanStack/tanstack.com.git +git clone git@github.com:TanStack/form.git ``` -tanstack/ - ├── tanstack.com/ - ├── query/ - ├── router/ - └── table/ -``` - -> [!WARNING] -> Directory names must match repo names exactly (e.g., `query` not `tanstack-query`). The app finds docs by looking for sibling directories by name. - -### Editing Docs - -To edit docs for a project, make changes in its `docs/` folder (e.g., `../form/docs/`) and visit http://localhost:3000/form/latest/docs/overview to preview. > [!NOTE] -> Updated pages need to be manually reloaded in the browser. +> Your `tanstack` directory should look like this: +> +> ``` +> tanstack/ +> | +> +-- form/ +> | +> +-- tanstack.com/ +> ``` > [!WARNING] -> Update the project's `docs/config.json` if you add a new doc page! +> Make sure the name of the directory in your local file system matches the name of the project's repo. For example, `tanstack/form` must be cloned into `form` (this is the default) instead of `some-other-name`, because that way, the doc pages won't be found. -## Get Involved +3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode: -- We welcome issues and pull requests! -- Participate in [GitHub Discussions](https://github.com/TanStack/tanstack.com/discussions) -- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ) - -## Explore the TanStack Ecosystem +```sh +cd tanstack.com +pnpm i +# The app will run on https://localhost:3000 by default +pnpm dev +``` -- TanStack Config – Tooling for JS/TS packages -- TanStack DB – Reactive sync client store -- TanStack DevTools – Unified devtools panel -- TanStack Form – Type‑safe form state -- TanStack Pacer – Debouncing, throttling, batching -- TanStack Query – Async state & caching -- TanStack Ranger – Range & slider primitives -- TanStack Router – Type‑safe routing, caching & URL state -- TanStack Start – Full‑stack SSR & streaming -- TanStack Store – Reactive data store -- TanStack Table – Headless datagrids -- TanStack Virtual – Virtualized rendering +4. Now you can visit http://localhost:3000/form/latest/docs/overview in the browser and see the changes you make in `tanstack/form/docs`. -… and more at TanStack.com » +> [!NOTE] +> The updated pages need to be manually reloaded in the browser. - +> [!WARNING] +> You will need to update the `docs/config.json` file (in the project's repo) if you add a new doc page! diff --git a/app.config.ts b/app.config.ts new file mode 100644 index 000000000..f4295a5a1 --- /dev/null +++ b/app.config.ts @@ -0,0 +1,54 @@ +import { sentryVitePlugin } from '@sentry/vite-plugin' +import { defineConfig } from '@tanstack/start/config' +import contentCollections from '@content-collections/vite' +import tsConfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + server: { + preset: 'netlify', + }, + vite: { + plugins: [ + tsConfigPaths(), + (() => { + const replacements = [ + // replace `throw Error(p(418))` with `console.error(p(418))` + ['throw Error(p(418))', 'console.error(p(418))'], + // replace `throw new Error('Hydration failed` with `console.error('Hydration failed')` + [ + `throw new Error('Hydration failed`, + `console.error('Hydration failed`, + ], + ] as const + + return { + name: 'tanner-test', + enforce: 'post', + transform(code, id) { + replacements.forEach(([search, replacement]) => { + if (code.includes(search)) { + code = code.replaceAll(search, replacement) + } + }) + + return code + }, + } + })(), + ], + }, + routers: { + client: { + vite: { + plugins: [ + sentryVitePlugin({ + authToken: process.env.SENTRY_AUTH_TOKEN, + org: 'tanstack', + project: 'tanstack-com', + }), + contentCollections(), + ], + }, + }, + }, +}) diff --git a/app/auth/auth.ts b/app/auth/auth.ts new file mode 100644 index 000000000..7cf126840 --- /dev/null +++ b/app/auth/auth.ts @@ -0,0 +1,76 @@ +import { createCookie } from '@remix-run/node' +import { redirect } from '@tanstack/react-router' + +let secret = process.env.COOKIE_SECRET || 'default' +if (secret === 'default') { + console.warn( + '🚨 No COOKIE_SECRET environment variable set, using default. The app is insecure in production.' + ) + secret = 'default-secret' +} + +let cookie = createCookie('auth', { + secrets: [secret], + // 30 days + maxAge: 30 * 24 * 60 * 60, + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', +}) + +export async function getAuthFromRequest( + request: Request +): Promise { + const c = request.headers.get('Cookie') + let userId = await cookie.parse(c) + return userId ?? null +} + +export async function setAuthOnResponse( + response: Response, + userId: string +): Promise { + let header = await cookie.serialize(userId) + response.headers.append('Set-Cookie', header) + return response +} + +export async function requireAuthCookie(request: Request) { + let userId = await getAuthFromRequest(request) + if (!userId) { + throw redirect({ + to: '/login', + headers: { + 'Set-Cookie': await cookie.serialize('', { + maxAge: 0, + }), + }, + }) + } + return userId +} + +export async function redirectIfLoggedInLoader({ + request, +}: { + request: Request +}) { + let userId = await getAuthFromRequest(request) + if (userId) { + throw redirect({ + to: '/', + }) + } + return null +} + +export async function redirectWithClearedCookie() { + return redirect({ + to: '/', + headers: { + 'Set-Cookie': await cookie.serialize(null, { + expires: new Date(0), + }), + }, + }) +} diff --git a/app/blog/ag-grid-partnership.md b/app/blog/ag-grid-partnership.md new file mode 100644 index 000000000..56547e1ec --- /dev/null +++ b/app/blog/ag-grid-partnership.md @@ -0,0 +1,21 @@ +--- +title: TanStack Table + Ag-Grid Partnership +published: 2022-06-17 +authors: + - Tanner Linsley + - Niall Crosby +--- + +We're excited to announce that [AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable) is now the official **TanStack Table** open-source partner! Together we will strive to achieve the following goals: + +- To jointly educate the JavaScript and TypeScript ecosystem about the differences between the two libraries and when to choose which. +- To cover as many use-cases as possible across the ecosystem by encouraging the usage of the other when goals and limitations of either are not met. +- To improve the quality of both libraries through shared experience, knowledge and even code when appropriate. + +TanStack Table and [AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable) share the same general problem space, but are implemented via drastically different architectures and paradigms, each offering unique trade-offs, opinions and optimizations depending on use-case. These differences and trade-offs are complimentary to one another and together form what we believe are the best two datagrid/table options available in the JavaScript and TypeScript ecosystem. + +To learn about the differences and trade-offs between the two libraries, start by reading the [introduction to TanStack Table](/table/v8/docs/introduction) and the [introduction to AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable)! + +We are excited about the future of datagrids and tables and we at TanStack are honored that AG Grid is invested in the success of it's open source ecosystem! + +- [Tanner Linsley](https://twitter.com/tannerlinsley) & [Niall Crosby](https://twitter.com/niallcrosby) diff --git a/app/blog/announcing-tanstack-form-v1.md b/app/blog/announcing-tanstack-form-v1.md new file mode 100644 index 000000000..e55750ffa --- /dev/null +++ b/app/blog/announcing-tanstack-form-v1.md @@ -0,0 +1,186 @@ +--- +title: Announcing TanStack Form v1 +published: 2025-03-03 +authors: + - Corbin Crutchley +--- + +![TanStack Form v1](/blog-assets/announcing-tanstack-form-v1/form_header.png) + +We're excited to announce the first stable version of [TanStack Form](/form/v1) is live and ready for usage in production! 🥳 + +We support five frameworks at launch: React, Vue, Angular, Solid, and Lit, as well as a myriad of features for each specific framework. + +# How to install + +```shell +$ npm i @tanstack/react-form +# or +$ npm i @tanstack/vue-form +# or +$ npm i @tanstack/angular-form +# or +$ npm i @tanstack/solid-form +# or +$ npm i @tanstack/lit-form +``` + +# A bit of history + +It was nearly two years ago when [I saw Tanner's BlueSky (an invite-only platform at the time) post announcing that he was working on a new project: TanStack Form](https://bsky.app/profile/tannerlinsley.com/post/3ju5z473w5525). + +![A back and forth between Tanner and myself on Bluesky about TanStack Form](/blog-assets/announcing-tanstack-form-v1/tanstack_form_bluesky_announce.png) + +At the time, I had just launched an alternative form library for React called "[HouseForm](https://web.archive.org/web/20240101000000*/houseform.dev)" and I was immediately enamored by some of the ideas Tanner's library brought to the table. + +I was fortunate enough to attend a hackathon that Tanner was also going to soon after and we were able to get some time to work on integrating some APIs from HouseForm into the project. + +Since that time, Tanner's handed much of the reigns of Form over to me and a wonderful group of additional maintainers. + +So, what have we built in that time? + +# Features + +One of the advantages of being in the oven for so long is that TanStack Form launches with a flurry of features you can leverage day one. + +Let's go over _just a few_ of them using React's adapter as examples. + +## Extreme type safety + +Like many all of the TanStack projects, Form has revolutionized what it means to be a "type-safe" form library. + +```tsx +const form = useForm({ + defaultValues: { + name: "", + age: 0 + } +}); + +// TypeScript will correctly tell you that `firstName` is not a valid field + + +// TypeScript will correctly tell you that `name`'s type is a `string`, not a `number` + }/> +``` + +We even support type-checking what errors are returned in ``: + +```tsx + (value < 12 ? { tooYoung: true } : undefined), + }} + children={(field) => ( + <> + + // TypeScript will correctly tell you that `errorMap.onChange` // is an object, + not a string +

{field.state.meta.errorMap.onChange}

+ + )} +/> +``` + +> Oh, yeah, we support field-based validation as well as form validation. Mix-n-match them! + +The best part? [You won't need to pass any typescript generics to get this level of type safety](/form/latest/docs/philosophy#generics-are-grim). Everything is inferred from your runtime usage. + +## Schema validation + +Thanks to the awesome work by the creators of [Zod](http://zod.dev/), [Valibot](https://valibot.dev), and [ArkType](https://arktype.io/), we support [Standard Schema](https://github.com/standard-schema/standard-schema) out of the box; no other packages needed. + +```tsx +const userSchema = z.object({ + age: z.number().gte(13, 'You must be 13 to make an account'), +}) + +function App() { + const form = useForm({ + defaultValues: { + age: 0, + }, + validators: { + onChange: userSchema, + }, + }) + return ( +
+ { + return <>{/* ... */} + }} + /> +
+ ) +} +``` + +## Async validation + +That's not all, though! We also support async functions to validate your code; complete with built-in debouncing and [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)-based cancellation: + +```tsx + { + const currentAge = await fetchCurrentAgeOnProfile({ signal }) + return value < currentAge ? 'You can only increase the age' : undefined + }, + }} +/> +``` + +## Platform support + +Not only do we support multiple frameworks as we mentioned from the start; we support multiple runtimes. Whether you're using React Native, NativeScript, or even SSR solutions like Next.js or [TanStack Start](/start), we have you covered. + +In fact, if you're using SSR solutions, we even make server-side form validation a breeze: + +```typescript +// app/routes/index.tsx, but can be extracted to any other path +import { createServerValidate, getFormData } from '@tanstack/react-form/start' +import { yourSchemaHere } from '~/constants/forms' + +const serverValidate = createServerValidate({ + ...formOpts, + onServerValidate: yourSchemaHere, +}) + +export const getFormDataFromServer = createServerFn({ method: 'GET' }).handler( + async () => { + return getFormData() + } +) +``` + +> This code sample excludes some of the relevant code to keep things glanceable. [For more details on our SSR integration, please check our docs.](/form/latest/docs/framework/react/guides/ssr) + +And boom, the exact same validation logic is running on both your frontend and backend. Your forms will even show errors when JavaScript is disabled on the user's browser! + +# A look forward + +We're not resting on our laurels, however - we have plans to add new features to v1 now that we're stable. These features include: + +- [Persistence APIs](https://github.com/TanStack/form/pull/561) +- [A Svelte 5 adapter](https://github.com/TanStack/form/issues/516) +- [Better DX for transforming values on submission](https://github.com/TanStack/form/issues/418) +- [Form Groups](https://github.com/TanStack/form/issues/419) + +And much more. + +# Thank **you** + +There's so many people I'd like to thank that once I'd gotten started I'd never end. Instead, I'll address each group of folks I want to thank. + +- Thank you to our contributors: So many people had to come together to make this happen. From maintainers of other TanStack projects giving us guidance, to drive-by PRs; it all helped us get across the line. + +- Thank you to our early adopters: The ones who took a risk on us and provided invaluable feedback on our APIs and functionality. +- Thank you to the content creators who covered our tools: You brought more eyes to our project - making it better through education and feedback. +- Thank you to the broader community: Your excitement to use our tools have driven the team immensely. + +And finally, thank **you** for taking the time to read and explore our newest tool. ❤️ diff --git a/app/blog/announcing-tanstack-query-v4.md b/app/blog/announcing-tanstack-query-v4.md new file mode 100644 index 000000000..ea584c310 --- /dev/null +++ b/app/blog/announcing-tanstack-query-v4.md @@ -0,0 +1,59 @@ +--- +title: Announcing TanStack Query v4 +published: 2022-07-14 +authors: + - Dominik Dorfmeister +--- + +We're excited to announce the next version of [TanStack Query](/query/v4), previously known as `react-query` 🎉. +The rebranding and restructuring to a monorepo now finally allows us to bring the joy of `react-query` to other frameworks, like `vue`, `svelte` or `solid`. + +TanStack Query is built upon an agnostic core with framework specific adapters on top of it. This allows us to share the core logic that make TanStack Query awesome like the QueryClient or Query Subscriptions between frameworks, while also having framework specific code like hooks (useQuery and useMutation) inside adapters. + +## How to install + +``` +npm i @tanstack/react-query +# or +yarn add @tanstack/react-query +``` + +## New Features + +### Proper offline support + +v4 has evolved TanStack Query from a data-fetching library to a true async state manager. All assumptions that were previously taken about having to have an active network connection are now gone, so TanStack Query _really_ works with any Promise, no matter how you produce it. +To achieve this, we've introduced a brand new [Network Mode](/query/v4/docs/guides/network-mode) that helps TanStack Query to decide when queries should respect being online or not. + +### Stable Persisters + +Since v3, persisters existed as an experimental feature. They allow you to sync the Query Cache to an external location (e.g. localStorage) for later use. We have revamped and improved the APIs after getting lots of feedback, and we are now providing two main peristers out of the box: + +- SyncStoragePersister +- AsyncStoragePersister + +Those persisters works pretty well for most use-cases, but nothing stops you from writing your own persister - as long it adheres to the required interface. + +### Support for React 18 + +React 18 was released earlier this year, and v4 now has first class support for it and the new concurrent features it brings. To achieve that, internal subscriptions were re-written to leverage the new `useSyncExternalStore` hook. + +### Tracked Queries per default + +Tracked Queries are a performance optimization that were added in v3.6.0 as an opt-in feature. This optimization is now the default behaviour in v4, which should give you a nice boost in render performance. + +### Streamlined APIs + +Over time, some of our APIs have become quite complex, to the extent that they were contradicting each other. Some examples include: + +- QueryKeys sometimes being converted to an Array when exposed, sometimes not. +- Query Filters being unintuitive and mutually exclusive. +- Default values for parameters defaulting to opposite values on different methods + +We've cleaned up a lot of these inconsistencies to make the developer experience even better. v4 also comes with a codemod to help you with the migration path. + +## What's next? + +Cleaning up the docs, for one. As you might have noticed, they are still pretty react specific and reference `react-query` from time to time. Please bear with us as we aim to restructure the docs, and PRs are always welcome. + +Also, more adapters. Currently, only the React adapter exists, but we are eager to add more frameworks soon. diff --git a/app/blog/announcing-tanstack-query-v5.md b/app/blog/announcing-tanstack-query-v5.md new file mode 100644 index 000000000..49203db93 --- /dev/null +++ b/app/blog/announcing-tanstack-query-v5.md @@ -0,0 +1,62 @@ +--- +title: Announcing TanStack Query v5 +published: 2023-10-17 +authors: + - Dominik Dorfmeister +--- + +About one year ago, we announced the [TanStack Query v5 roadmap](https://github.com/TanStack/query/discussions/4252), and the whole team has been working hard on that version ever since. So we're super happy to announce that today is the day: After 91 alpha releases, 35 betas and 16 release candidates, TanStack Query [v5.0.0](https://github.com/TanStack/query/releases/tag/v5.0.0) is finally here! 🎉 + +v5 continues the journey of v4, trying to make TanStack Query smaller (v5 is ~20% smaller than v4), better and more intuitive to use. One of the main focus points for this release was around streamlining and simplifying the APIs we offer: + +## Breaking changes + +As a big breaking change, we've removed most overloads from the codebase, unifying how you use `useQuery` and other hooks. This is something we wanted to do for v4, but a [TypeScript limitation](https://github.com/microsoft/TypeScript/issues/43371) prevented us from doing that. TypeScript addressed this issue in TS 4.7, so we were able to remove all the overloads that we had for calling `useQuery` with a different amount of parameters. This is a huge DX win, because methods with overloads usually have quite bad TypeScript error messages. + +This is the biggest breaking change in v5, but we think it's worth it. The API is now much more consistent - you always just pass _one_ object. To alleviate the pain of changing all occurrences manually, we have tried to prepare everyone for this coming change for the last months. The documentation was changed to use the new API, and we released an auto-fixable [eslint rule](/query/v4/docs/eslint/prefer-query-object-syntax) in our eslint package. Additionally, v5 comes with [a codemod](/query/v5/docs/react/guides/migrating-to-v5#codemod) to help with the transition. + +Apart from that, we've renamed `cacheTime` to `gcTime` to better reflect what it is doing, merged `keepPreviousData` with `placeholderData`, renamed `loading` states to `pending` and [removed the callbacks](https://github.com/TanStack/query/discussions/5279) from `useQuery`. All these changes make v5 the most consistent and best version for new starters. + +To read more about the breaking changes, have a look at our [migration guide](/query/v5/docs/react/guides/migrating-to-v5). + +## New Features + +Of course, v5 comes loaded with amazing new features as well 🚀: + +### Simplified optimistic updates + +Enjoy a brand new, simplified way to perform optimistic updates by leveraging the returned `variables` from `useMutation`, without having to write code that updates the cache manually. For more details, have a look at the [optimistic updates documentation](/query/v5/docs/react/guides/optimistic-updates) + +### Sharable mutation state + +A frequently requested feature, as seen in this [two-year-old issue](https://github.com/TanStack/query/issues/2304), finally comes to life in v5: You can now get access to the state of all mutations, shared across components thanks to the new [useMutationState](/query/v5/docs/react/reference/useMutationState) hook. + +### 1st class `suspense` support + +That's right - `suspense` for data fetching is no longer experimental, but fully supported. React Query ships with new `useSuspenseQuery`, `useSuspenseInfiniteQuery` and `useSuspenseQueries` hooks. Have a look at the [suspense docs](/query/v5/docs/react/guides/suspense) to learn about the differences to the non-suspense versions. + +#### Streaming with React Server Components + +v5 also comes with an experimental integration for suspense on the server in nextJs, unifying the best of both worlds: The [react-query-next-experimental](/query/v5/docs/react/guides/advanced-ssr#experimental-streaming-without-prefetching-in-nextjs) adapter allows us to write a single `useSuspenseQuery`, which will initiate data fetching as early as possible: on the server, during SSR. It will then stream the result to the client, where it will be put into the cache automatically, giving us all the interactivity and data synchronization of React Query. + +### Improved Infinite Queries + +Infinite Queries can now [prefetch multiple pages](/query/v5/docs/react/guides/prefetching) at once, and you have the option to specify the [maximum amount of pages](/query/v5/docs/react/guides/infinite-queries#what-if-i-want-to-limit-the-number-of-pages) stored in the cache as well. + +### New Devtools + +The Query devtools have been re-written from scratch in a framework-agnostic way to make them available to all adapters. They also got a UI revamp and some new features like cache inline editing and light mode. + +### Fine-grained persistence + +Another long-standing [discussion from 2021](https://github.com/TanStack/query/discussions/2649) highlights the importance of fine-grained persistence with just-in-time restore capabilities (especially for mobile development) that the `PersistQueryClient` plugin doesn't have. With v5, we now have a new [experimental_createPersister](/query/v5/docs/react/plugins/createPersister) plugin that allows you to persist queries individually. + +### The `queryOptions` API + +Now that we have a unified way to call `useQuery` (with just one object as parameter), we can also build better abstractions on top of that. The new [queryOptions](/query/v5/docs/react/typescript#typing-query-options) function gives us a type-safe way to share our query definitions between `useQuery`and imperative methods like `queryClient.prefetchQuery`. On top of that, it can make `queryClient.getQueryData` type-safe as well. + +--- + +## THANK YOU + +We hope you're going to enjoy using v5 as much as we've enjoyed building it. What's left for us to say thanks to everyone who made this release possible. No matter if you're core contributor, implemented an issue from the roadmap, if you've fixed a typo in the docs or gave feedback on the alpha releases: Every contribution matters! It's the people that makes this library great, and we're blessed to have such an amazing community. ❤️ diff --git a/app/blog/netlify-partnership.md b/app/blog/netlify-partnership.md new file mode 100644 index 000000000..f3740959d --- /dev/null +++ b/app/blog/netlify-partnership.md @@ -0,0 +1,46 @@ +--- +title: TanStack + Netlify Partnership +published: 2025-03-18 +authors: + - Tanner Linsley +--- + +![Netlify Header](/blog-assets/netlify-partnership/header.jpg) + +We’re excited to announce that **Netlify** is now the **Official Deployment Partner** for **TanStack Start**! Together, we’re making it easier than ever for developers to build and deploy modern, type-safe, and user-focused web applications. + +Netlify has earned its reputation as the ultimate deployment platform for modern web developers. Its focus on speed, simplicity, modularity, and flexibility aligns perfectly with TanStack Start’s vision for full-stack development. Here’s why Netlify stands out: + +- **No-config simplicity** – Deploy your TanStack Start apps in seconds, with zero setup hassle. +- **Serverless power** – Netlify Functions enable dynamic, real-time features effortlessly. +- **Global performance** – Fast, reliable apps served from Netlify’s global edge network. +- **Developer-first tools** – Instant previews, automated workflows, and seamless integrations make building a joy. + +## Why Netlify? + +Netlify is more than just a deployment provider—they’ve worked closely with us to ensure that deploying TanStack Start applications is not just fast, but optimized for the best possible developer experience. Whether you’re building interactive UIs, data-heavy dashboards, real-time tools, or AI-powered applications, Netlify’s platform makes the process seamless. + +As part of this partnership, Netlify has also launched a **full-stack AI chatbot starter template** that showcases TanStack Start’s powerful data management capabilities alongside Netlify Functions. This template provides: + +- **Real-time data handling** with TanStack Query +- **Efficient routing** with TanStack Router +- **Seamless server function integration** with Netlify +- **A production-ready deployment configuration** + +To get started with the template, simply run: + +``` +npx create-tsrouter-app@latest --template file-router --add-ons tanchat +``` + +This template is a great way to explore how TanStack Start and Netlify work together to simplify modern web development. + +## What’s Next? + +We’re just getting started. Expect more updates, new features, and deeper collaboration between TanStack Start and Netlify. Stay tuned for success stories, guides, and real-world examples showcasing what’s possible with this powerful combination. + +Additionally, join us March 31 for a **special TanStack Start episode on [Netlify’s Remote Desk series](https://www.netlify.com/webinars/netlify-remote-desk/)**. We’ll dive into live demos, developer tips, and an exclusive Q&A to show how to unlock the full potential of TanStack Start on Netlify. + +**Ready to dive in?** Check out the [TanStack Start docs](/start/latest/docs/framework/react/overview), explore the deployment guides, and start building with Netlify today. + +A huge thank you to Netlify for supporting and empowering developers. Let’s build something incredible together! 🚀 diff --git a/app/blog/tanstack-router-typescript-performance.md b/app/blog/tanstack-router-typescript-performance.md new file mode 100644 index 000000000..8c3d754dc --- /dev/null +++ b/app/blog/tanstack-router-typescript-performance.md @@ -0,0 +1,241 @@ +--- +title: A milestone for TypeScript Performance in TanStack Router +published: 2024-09-17 +authors: + - Christopher Horobin +--- + +TanStack Router pushes the boundaries on type-safe routing. + +The router's components such as `` and its hooks such as `useSearch`, `useParams`, `useRouteContext` and `useLoaderData`, infer from the route definitions to offer great type-safety. It's common for applications using TanStack Router to use external dependencies with complex types for `validateSearch`, `context`, `beforeLoad` and `loader` in their route definitions. + +While the DX is great, when route definitions accumulate into a route tree and it becomes large, the editor experience can start to appear slow. We've made many TypeScript performance improvements to TanStack Router so that issues only start to appear when the inference complexity becomes very large. We closely watch diagnostics such as instantiations and try to reduce the time TypeScript takes to type-check each individual route definition. + +Despite all these past efforts (which certainly helped), we had to address the elephant in the room. The fundamental problem to solve for a great editor experience in TanStack Router was not necessarily related to the overall typescript check time. The problem we've been working to resolve is the bottleneck in the TypeScript language service when it comes to type-checking the accumulated route tree. For those familiar with tracing TypeScript, a trace for a large TanStack Router application could look something similar to the following: + +![Tracing showing the route tree being inferred](/blog-assets/tsr-perf-milestone/tracing-slow.png) + +For those who don't know, you can generate a trace from TypeScript with the following: + +``` +tsc --generatetrace trace +``` + +This example has 400 route definitions all with `validateSearch` using `zod` and TanStack Query integration through the routes `context` and `loader`'s - it is an extreme example. The large wall at the beginning of the trace is what TypeScript was type-checking when it first hit an instance of the `` component. + +The language server works by type-checking a file (or a region of a file) from the beginning, but only for that file/region. So this meant that the language service had to do this work whenever you interacted with an instance of a `` component. It turns out, that this was the bottleneck that we were hitting when inferring all the necessary types from the accumulated route tree. As mentioned, route definitions themselves can contain complex types from external validation libraries which then also need inference. + +It became quite apparent early on that this was quite clearly going to slow down the editor experience. + +## Breaking down work for the language service + +Ideally, the language service should only need to infer from a route definition, based on where a `` is navigating `to`, instead of having to crawl the whole route tree. This way the language service would not need to busy itself with inferring the types of route definitions that are not the navigation target. + +Unfortunately, code-based route trees rely on inference to build the route tree which triggers the wall shown in the trace above. However, TanStack Router's file-based routing, has the route tree being automatically generated whenever a route is created or modified. This meant that there was some exploration to be done here to see if we could eke out some better performance. + +Previously route trees were created like the following, even for file-based routing: + +```tsx +export const routeTree = rootRoute.addChildren({ + IndexRoute, + LayoutRoute: LayoutRoute.addChildren({ + LayoutLayout2Route: LayoutLayout2Route.addChildren({ + LayoutLayout2LayoutARoute, + LayoutLayout2LayoutBRoute, + }), + }), + PostsRoute: PostsRoute.addChildren({ PostsPostIdRoute, PostsIndexRoute }), +}) +``` + +Generating the route tree came as a consequence of reducing the tedious configuration of a route tree but keeping inference where it matters. This is where the first important change is introduced leading to better editor performance. Instead of inferring the route tree, we can take advantage of this generation step to _declare the route tree_. + +```tsx +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + PostsRoute: typeof PostsRouteWithChildren +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + PostsRoute: PostsRouteWithChildren, +} + +export const routeTree = rootRoute._addFileChildren(rootRouteChildren) +``` + +Note the use of an `interface` to declare the children to compose the route tree. This process is repeated for all routes and their children when generating the route tree. With this change, running a trace gave us a much better idea of what was happening inside the language-service. + +![Tracing showing the route tree being declared](/blog-assets/tsr-perf-milestone/tracing-declare-route-tree.png) + +This is still slow and we're not quite there yet but there is something - _the trace is different_. The type inference for the entire route tree was still happening, but it was now being done _somewhere else_. After working through our types, it turned out to be happening in a type named `ParseRoute`. + +```tsx +export type ParseRoute = TRouteTree extends { + types: { children: infer TChildren } +} + ? unknown extends TChildren + ? TAcc + : TChildren extends ReadonlyArray + ? ParseRoute + : ParseRoute + : TAcc +``` + +This type walks down the route tree to create a union of all routes. The union in turn is used to create a type mapping from `id` -> `Route`, `from` -> `Route` and also `to` -> `Route`. An example of this mapping exists as a mapped type. + +```tsx +export type RoutesByPath = { + [K in ParseRoute as K['fullPath']]: K +} +``` + +The important realization here was that when using file-based routing, we were able to skip the `ParseRoute` type entirely by outputting that mapping type ourselves whenever the route tree was generated. Instead, we'd be able to generate the following: + +```tsx +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/posts': typeof PostsRouteWithChildren + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts': typeof PostsIndexRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/posts': typeof PostsRouteWithChildren + '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute + '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute + '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/posts' + | '/posts/$postId' + | '/posts/' + | '/layout-a' + | '/layout-b' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/posts/$postId' | '/posts' | '/layout-a' | '/layout-b' + id: + | '__root__' + | '/' + | '/_layout' + | '/posts' + | '/_layout/_layout-2' + | '/posts/$postId' + | '/posts/' + | '/_layout/_layout-2/layout-a' + | '/_layout/_layout-2/layout-b' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + PostsRoute: typeof PostsRouteWithChildren +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + PostsRoute: PostsRouteWithChildren, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() +``` + +In addition to declaring children, we also declare interfaces which map paths to a route. + +This change along with other type level changes to conditionally use `ParseRoute` only if these types are not registered resulted in a trace which was our aim all along 🥳 + +![Tracing route tree declaration being inferred faster](/blog-assets/tsr-perf-milestone/tracing-faster.png) + +The first file to reference a `` no longer triggers inference from the whole route tree which increases perceived language service speed. significantly + +By doing this, TypeScript will infer the types required for a specific route when it is referenced by a ``. This may not translate to overall better TypeScript type-checking time when all routes are being linked to, but it is a significant speed increase for the language service when in a file/region. + +The difference between the two is striking, as seen in these large route trees with complex inference (400 in this example below): + +
+ + +
+ +You may be thinking that this is _cheating_ since we are doing a lot of the heavy lifting here in the route tree generation phase. Our response to that is that this generation step for file-based routing (and now virtual file-based routing) was already there and was always a necessary step whenever you modified or created a new route. + +So, once a route has been created and the route tree is generated, the inference remains the same throughout everything within the route definition. This means that you can make changes to `validateSearch`, `beforeLoad`, `loader`, and others, with the inferred types always being reflected instantly. + +The DX has not changed, but the performance in your editor feels awesome (especially when you are working with large route trees). + +## The ground rules + +This change involved a lot of TanStack Router's exports being improved to make it more performant to consume these generated types whilst still being able to fall back on the whole route tree inference when using code-based routing. We've also still got areas of the codebase that still rely on the full route tree inference. These areas are our version of a loose/non-strict mode. + +```tsx + + + ({..prev, page: 0 })} /> +``` + +All three above usages of ``, require the inference of the whole route tree and therefore result in a worse editor experience when interacting with them. + +In the first two instances, TanStack Router does not know what route you want to navigate to, therefore it tries its best to guess at a very loose type inferred from all the routes in your route tree. The third instance of `` from above, uses the `prev` argument in the `search` updater function, but in this instance, TanStack Router does not know which `Route` you are navigating `from`, whereby it needs to once again try to guess the loose type of `prev` by scanning the whole route tree. + +The most performant usage of `` in your editor would be the following: + +```tsx + + + ({...prev, page: 0 })} /> +``` + +TanStack Router can narrow the types to specific routes in these cases. This means that you get better type safety and better editor performance as your application scales. Therefore, we encourage the use of `from` and/or `to` in these cases. To be clear, in the third example, the usage of `from` is only necessary if the `prev` argument is used, otherwise, TanStack Router does not need to infer the whole route tree. + +These looser types also occur in `strict: false` modes. + +```tsx +const search = useSearch({ strict: false }) +const params = useParams({ strict: false }) +const context = useRouteContext({ strict: false }) +const loaderData = useLoaderData({ strict: false }) +const match = useMatch({ strict: false }) +``` + +In this case, better editor performance and type safety can be achieved by using the recommended `from` property. + +```tsx +const search = useSearch({ from: '/dashboard' }) +const params = useParams({ from: '/dashboard' }) +const context = useRouteContext({ from: '/dashboard' }) +const loaderData = useLoaderData({ from: '/dashboard' }) +const match = useMatch({ from: '/dashboard' }) +``` + +## Moving forward + +Going forward, we believe that TanStack Router is very well positioned to have the best balance between type safety and TypeScript performance, without having to compromise on the quality of the type inference used throughout the route definitions in file-based (and virtual file-based) routing. Everything in your route definitions remains inferred, with the changes in the generated route tree only aiding the language service by declaring the necessary types where it matters, something that you would never want to write yourself. + +This approach also looks to be scalable for the language service. We were able to create thousands of route definitions with the language service remaining stable provided you keep to the `strict` parts of TanStack Router. + +We will continue to keep improving TypeScript performance on TanStack Router to reduce overall check time and improve the language service performance further, but still felt that this was an important milestone to share and something we hope the users of TanStack Router will appreciate. diff --git a/app/blog/why-tanstack-start-and-router.md b/app/blog/why-tanstack-start-and-router.md new file mode 100644 index 000000000..008cc83d4 --- /dev/null +++ b/app/blog/why-tanstack-start-and-router.md @@ -0,0 +1,74 @@ +--- +title: Why choose TanStack Start and Router? +published: 2024-12-03 +authors: + - Tanner Linsley +--- + +![TanStack Start and Router Blog Header](/blog-assets/why-tanstack-start-and-router/tanstack-start-blog-header.jpg) + +Building modern web applications is no small feat. The frameworks and tools we choose can make or break not only our developer experience but also the success of the applications we build. While there are many great frameworks out there, I believe **TanStack Router** and **TanStack Start** stand apart for their ability to solve the challenges developers face today and their readiness for what’s coming tomorrow. + +These aren’t just another set of tools—they represent a commitment to building better apps with less friction and more joy. Here’s why I think you’ll love working with them as much as I do. + +### Type Safety You Can Rely On + +Type safety isn’t just a buzzword—it’s a foundational tool for creating robust, maintainable applications. TanStack Router goes beyond the basics to offer **contextual type safety**, where types flow seamlessly through every part of your app. Route definitions, parameters, navigation, and even state management all work together with fully inferred types. + +What does this mean for you? It means no more guessing if you’ve defined a parameter correctly, no more debugging mismatched types, and no need for extra plugins or AST transformations to fill in the gaps. TanStack Router works with TypeScript’s natural architecture, making the experience smooth, predictable, and delightful. + +This level of type safety doesn’t just save time—it builds confidence. And it’s something I believe other frameworks will spend years trying to catch up to. + +### Unlock the Power of URL State Management + +One of the most overlooked but powerful tools in web development is the URL. It’s the original state management system—fast, shareable, and intuitive. Yet, many frameworks treat the URL as an afterthought, offering only basic utilities for reading and writing state. + +TanStack Router flips that script. Managing state in the URL isn’t just supported—it’s encouraged. With intuitive APIs, you can validate, read, and update search parameters with type safety and runtime validation baked in. Want to create a deeply nested, dynamic filter system or synchronize your app’s state with the URL? It’s effortless. + +But this isn’t just about developer convenience—it’s about creating better user experiences. When your app state lives in the URL, users can share it, bookmark it, and pick up right where they left off. TanStack Router makes that as easy as it should be. + +### Familiar Patterns, More Flexibility + +If you’ve worked with Remix or Next.js, you’ll find plenty of familiar concepts in TanStack Start. But familiarity doesn’t mean compromise. We’ve taken some of the best ideas from those frameworks and pushed them further, stripping away the constraints and introducing more flexibility. + +For example, routing patterns and server function integrations will feel natural if you’re coming from a server-first framework like Remix, but they’re designed to work just as well for traditional client-side SPAs. You don’t have to pick a side—you get the best of both worlds, with fewer trade-offs. + +### Built for the Future (and Already Embracing It) + +The web is changing fast. With React Server Components (RSCs) on the horizon, React 19 introducing new patterns, and streaming becoming the standard for data delivery, frameworks need to do more than just keep up—they need to lead. + +TanStack Start is ready for what’s next. RSCs are treated as another server-side state, with powerful primitives for caching, invalidating, and composing them into your application. Streaming isn’t an afterthought—it’s baked into the core of how TanStack works, giving you tools to incrementally send data and HTML to the client without extra complexity. + +But we’re not just about future-proofing. TanStack Start also makes these advanced capabilities approachable and usable today. You don’t need to wait for the “next big thing” to start building apps that feel like the future. + +### SPAs Aren’t Dead (We’re Just Making Them Better) + +There’s a lot of talk about server-first architectures these days, and while they’re exciting, they’re not the whole story. Single Page Applications (SPAs) are still an incredible way to build fast, interactive apps—especially when done right. + +TanStack Start doesn’t just keep SPAs viable—it makes them better. With simplified patterns, powerful state management, and deep integrations, you can build SPAs that are more performant, easier to maintain, and a joy to use. Whether you’re working server-first, client-first, or somewhere in between, TanStack gives you the tools to build the app you want. + +### Data Integration Like No Other + +If you’ve used **TanStack Query**, you already know how much it simplifies data-fetching. But the integration between TanStack Query and TanStack Router is where the magic really happens. Prefetching data, caching results, and streaming updates are all seamless, intuitive, and built to scale. + +For example, you can prefetch data in a route loader, stream it down to the client, and hydrate it on demand—all with a single API. Whether you’re managing a simple blog or a complex dashboard, you’ll find yourself spending less time wiring up data and more time building features. + +This isn’t just an integration—it’s a partnership between routing and data-fetching that makes everything else feel clunky by comparison. + +### Routing That Scales + +Routing isn’t just a utility—it’s the backbone of every application. And yet, most routers struggle when things get complex. That’s where TanStack Router shines. It’s built to handle everything from a handful of simple routes to thousands of deeply nested ones without breaking a sweat. + +Features like type-safe navigation, hierarchical route contexts, and advanced state synchronization make it easy to build apps that scale in both size and complexity. And because TanStack Router works natively with TypeScript, you get all the benefits of type safety without sacrificing performance or flexibility. + +### Always Innovating + +What excites me most about TanStack is that we’re just getting started. From isomorphic server functions to powerful caching primitives and streamlined React Server Component support, we’re constantly pushing the boundaries of what’s possible. Our goal isn’t just to build great tools—it’s to build tools that help _you_ build better apps. + +### Settle for “Good Enough”? + +Other frameworks have their strengths, but if you’re looking for tools that are innovative, flexible, and deeply integrated, TanStack Router and Start are in a league of their own. They’re not just solving today’s problems—they’re helping you build apps that are ready for tomorrow. + +So why wait? Explore [TanStack Router](https://tanstack.com/router) and [TanStack Start](https://tanstack.com/start) today, and see how much better app development can be. + +Let’s build something amazing together! diff --git a/app/blog/why-tanstack-start-is-ditching-adapters.md b/app/blog/why-tanstack-start-is-ditching-adapters.md new file mode 100644 index 000000000..5ee6e9908 --- /dev/null +++ b/app/blog/why-tanstack-start-is-ditching-adapters.md @@ -0,0 +1,88 @@ +--- +title: Why TanStack Start is Ditching Adapters +published: 2024-11-22 +authors: + - Tanner Linsley +--- + +![Nitro Header](/blog-assets/why-tanstack-start-is-ditching-adapters/nitro.jpg) + +## To “adapter” or not? + +Building a new front-end Javascript framework is a daunting task, as I’ve been learning with building TanStack Start, my new TanStack-powered full stack framework. There’s so many moving pieces: + +- Routing +- Server Side Rendering +- RPCs & APIs +- Development Tools +- **Deployment & Hosting** + +That last one, **deployment & hosting** can be especially tricky, since these days it seems that every single cloud environment has it’s own quirky incantations to get things to work “just right”. When faced with this decision for TanStack Start, I obviously knew which hosts I wanted to start supporting out of the gate and Vercel was at the top of that list. + +My first gut-reaction was to start building a system that could have “adapters” for each host, then just focus on writing the Vercel adapter first. + +This plan, however, was flawed from the start. It didn’t take long to realize that I personally was going to be responsible (at least in the beginning) for writing most if not all of the code to make TanStack Start come to life on not only Vercel, but many other targets and platforms. After some quick research, this task alone was daunting enough to make me question my motivations in building a server-bound JS framework at all. + +Technically, the work required to deploy to Vercel alone is already very simple by just adhering to some output file/directory naming conventions. However the paralysis came from just the sheer number of environments/hosts there are to support. There’s a lot! Just look at [Remix’s growing list of server adapters](https://remix.run/docs/en/main/other-api/adapter)! Remix isn’t the only framework with this list either. Most server-bound JS frameworks have something similar. I was essentially committing to writing at least 10 adapters in the first few months of the framework and I had barely gotten into the exciting features of the framework itself (not to mention the work in maintaining and updating these adapters). + +The harsh reality here is that there **isn’t a way around this.** If your framework is providing **any server-targeted code in your framework, you need to ensure it will run perfectly anywhere you can deploy it.** + +So, as I was about to succumb to the infinite sadness of writing a hundred adapters and dealing with upstream breaking changes for the rest of my life, I spoke with my friend [Ryan Carniato](https://twitter.com/ryancarniato) about how Solid JS is approaching this problem with our cousin framework [Solid Start](https://start.solidjs.com/) and he confidently said “Just use Vite and Nitro". + +## TanStack Start = Nitro + Vite + TanStack Router + +[Nitro](https://nitro.unjs.io/) is a “JavaScript toolkit to build server-side frameworks with your own opinions”, powered by [Vite](https://vite.dev/). So what makes it so special? + +There are a ton of awesome features in Nitro that make it extremely useful for building a framework, but one of the coolest pieces is that it’s powered by H3 and Vite. Nitro’s tagline is literally “create web servers with everything you need and **deploy them wherever you prefer**” (emphasis is mine). + +In simple terms, Nitro effectively makes TanStack Start “_adapter-less”._ It uses H3, an HTTP framework that maintains its own lower-level adapters on your behalf so you can write your server code once and use it anywhere (sounds a lot like React!). + +By using Nitro, all of TanStack Start’s adapter problems were gone. I never even had to think about them! + +In fact, to deploy to Vercel, it was even easier than I had initially planned: just pass a `vercel` target to our `defineConfig`’s `server.preset` option, which is passed to Nitro: + +```jsx +import { defineConfig } from '@tanstack/start/config' + +export default defineConfig({ + server: { + preset: 'vercel', + }, +}) +``` + +## What does it support? + +Nitro, H3 and Vite are **impressive to say the least.** We were pleased to see that on our first try, a slew of Vercel features worked perfectly out of the box including the GitHub integration, server functions, edge functions/middleware, immutable deploys, environment variables, server-side rendering, and even streaming. + +That’s a massive list that we essentially got for free by using Nitro and Vite. + +## TanStack Start is coming! + +With builds and deployments solved and built-in support for integrating my GitHub repos right into to my personal favorite hosting providers, I could finally focus on what I think makes TanStack Start special: + +- A best-in-class fully type-safe Router +- Flexible primitives for building server-bound RPCs +- Opt-in Server Functionality (SSR, APIs, RSCs, etc) +- And deep integration with other TanStack primitives like TanStack Query +- And even more to come! + +## Going the Extra Mile + +It’s awesome that we were able to offload so much to Nitro and Vite and gain so many awesome features, but it’s definitely not a 100% complete solution to using _every_ feature of a hosting platform, especially Vercel where we have access to more than just deployments. We’ve also been thinking more about features like [edge network caching](https://vercel.com/docs/edge-network/caching) and my personal favorite, [\*skew protection](https://vercel.com/docs/deployments/skew-protection).\* + +For instance, skew protection, which ensures that client and server stay in sync for their respective deployments requires more than just a build step. It involves the ability to deeply integrate platform primitives into the framework at runtime as well, or in the specific case, being able to inject specific cookies or headers into outgoing API/server requests directed at Vercel. + +I’m happy to report that TanStack Start is going to ship with amazingly powerful middleware primitives (for both API routes and Server Function RPCs) that will make this a one-liner, or possibly even automatic (hopefully). + +This level of DX and integration is what makes me excited for the future and I believe is what open source is truly about: composing powerful tools from the ecosystem together to deliver amazing experiences for both developer and user. + +I couldn’t think of a better mashup than TanStack Start, Nitro, Vite and Vercel to give you a best-in-class web app experience. + +## Try it in 60 seconds + +TanStack Start is currently in Beta! Click the ["Deploy"](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ftanstack%2Frouter%2Ftree%2Fmain%2Fexamples%2Freact%2Fbasic-file-based&project-name=my-tanstack-project&repository-name=my-tanstack-project) button below to both create and deploy a fresh copy of the TanStack Start “Basic” template to Vercel in ~1 minute. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ftanstack%2Frouter%2Ftree%2Fmain%2Fexamples%2Freact%2Fbasic-file-based&project-name=my-tanstack-project&repository-name=my-tanstack-project) + +We hope you enjoy what we’re building and can’t wait to get your feedback! diff --git a/app/blog/zh-hans/ag-grid-partnership.md b/app/blog/zh-hans/ag-grid-partnership.md new file mode 100644 index 000000000..0053beba3 --- /dev/null +++ b/app/blog/zh-hans/ag-grid-partnership.md @@ -0,0 +1,21 @@ +--- +title: TanStack Table + Ag-Grid 合作伙伴关系 +published: 2022-06-17 +authors: + - Tanner Linsley + - Niall Crosby +--- + +我们很高兴地宣布,[AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable) 现在是 **TanStack Table** 的官方开源合作伙伴!我们将共同努力实现以下目标: + +- 共同教育 JavaScript 和 TypeScript 生态系统关于两个库之间的差异以及何时选择哪个。 +- 通过在任一方的目标和限制不能满足时鼓励使用另一方,尽可能覆盖生态系统中的所有用例。 +- 通过共享经验、知识,甚至在适当时共享代码来提高两个库的质量。 + +TanStack Table 和 [AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable) 共享相同的一般问题空间,但通过截然不同的架构和范式实现,根据用例各自提供独特的权衡、观点和优化。这些差异和权衡是相互补充的,我们相信它们共同构成了 JavaScript 和 TypeScript 生态系统中可用的最佳两个数据网格/表格选项。 + +要了解两个库之间的差异和权衡,请从阅读 [TanStack Table 简介](/table/v8/docs/introduction) 和 [AG Grid 简介](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable) 开始! + +我们对数据网格和表格的未来感到兴奋,我们 TanStack 很荣幸 AG Grid 投资于其开源生态系统的成功! + +- [Tanner Linsley](https://twitter.com/tannerlinsley) & [Niall Crosby](https://twitter.com/niallcrosby) diff --git a/app/blog/zh-hans/announcing-tanstack-form-v1.md b/app/blog/zh-hans/announcing-tanstack-form-v1.md new file mode 100644 index 000000000..72f0672ac --- /dev/null +++ b/app/blog/zh-hans/announcing-tanstack-form-v1.md @@ -0,0 +1,185 @@ +--- +title: 宣布 TanStack Form v1 发布 +published: 2025-03-03 +authors: + - Corbin Crutchley +--- + +![TanStack Form v1](/blog-assets/announcing-tanstack-form-v1/form_header.png) + +我们很高兴地宣布 [TanStack Form](/form/v1) 的第一个稳定版本已经发布,可以在生产环境中使用了!🥳 + +我们在发布时支持五个框架:React、Vue、Angular、Solid 和 Lit,以及每个特定框架的大量功能。 + +# 如何安装 + +```shell +$ npm i @tanstack/react-form +# 或 +$ npm i @tanstack/vue-form +# 或 +$ npm i @tanstack/angular-form +# 或 +$ npm i @tanstack/solid-form +# 或 +$ npm i @tanstack/lit-form +``` + +# 一点历史 + +大约两年前,[我在 BlueSky(当时是一个仅限邀请的平台)上看到 Tanner 发布的帖子,宣布他正在开发一个新项目:TanStack Form](https://bsky.app/profile/tannerlinsley.com/post/3ju5z473w5525)。 + +![Tanner 和我在 Bluesky 上关于 TanStack Form 的来回对话](/blog-assets/announcing-tanstack-form-v1/tanstack_form_bluesky_announce.png) + +当时,我刚刚发布了一个名为 "[HouseForm](https://web.archive.org/web/20240101000000*/houseform.dev)" 的 React 替代表单库,我立即被 Tanner 的库带来的一些想法所吸引。 + +我很幸运能够参加 Tanner 也将参加的黑客马拉松,我们有机会花一些时间将 HouseForm 的一些 API 集成到项目中。 + +从那时起,Tanner 将 Form 的大部分工作交给了我和一群优秀的额外维护者。 + +那么,在这段时间里我们构建了什么? + +# 功能特性 + +长期开发的一个优势是 TanStack Form 在发布时就带有大量可以从第一天就开始使用的功能。 + +让我们用 React 适配器作为例子来介绍*其中的一些*功能。 + +## 极致的类型安全 + +像所有 TanStack 项目一样,Form 重新定义了"类型安全"表单库的含义。 + +```tsx +const form = useForm({ + defaultValues: { + name: "", + age: 0 + } +}); + +// TypeScript 会正确提示 `firstName` 不是一个有效的字段 + + +// TypeScript 会正确提示 `name` 的类型是 `string`,而不是 `number` + }/> +``` + +我们甚至支持检查 `` 中返回的错误类型: + +```tsx + (value < 12 ? { tooYoung: true } : undefined), + }} + children={(field) => ( + <> + + // TypeScript 会正确提示 `errorMap.onChange` 是一个对象,而不是字符串 +

{field.state.meta.errorMap.onChange}

+ + )} +/> +``` + +> 哦,是的,我们同时支持基于字段的验证和表单验证。可以混合搭配使用! + +最棒的是什么?[你不需要传递任何 typescript 泛型就能获得这种级别的类型安全](/form/latest/docs/philosophy#generics-are-grim)。一切都从你的运行时使用中推断出来。 + +## Schema 验证 + +感谢 [Zod](http://zod.dev/)、[Valibot](https://valibot.dev) 和 [ArkType](https://arktype.io/) 的创建者的出色工作,我们开箱即支持 [Standard Schema](https://github.com/standard-schema/standard-schema);不需要其他包。 + +```tsx +const userSchema = z.object({ + age: z.number().gte(13, '你必须年满 13 岁才能创建账户'), +}) + +function App() { + const form = useForm({ + defaultValues: { + age: 0, + }, + validators: { + onChange: userSchema, + }, + }) + return ( +
+ { + return <>{/* ... */} + }} + /> +
+ ) +} +``` + +## 异步验证 + +但这还不是全部!我们还支持使用异步函数来验证你的代码;包括内置的防抖和基于 [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) 的取消: + +```tsx + { + const currentAge = await fetchCurrentAgeOnProfile({ signal }) + return value < currentAge ? '你只能增加年龄' : undefined + }, + }} +/> +``` + +## 平台支持 + +正如我们从一开始就提到的,我们不仅支持多个框架;我们还支持多个运行时。无论你是使用 React Native、NativeScript,还是像 Next.js 或 [TanStack Start](/start) 这样的 SSR 解决方案,我们都能满足你的需求。 + +事实上,如果你使用 SSR 解决方案,我们甚至让服务器端表单验证变得轻而易举: + +```typescript +// app/routes/index.tsx,但可以提取到任何其他路径 +import { createServerValidate, getFormData } from '@tanstack/react-form/start' +import { yourSchemaHere } from '~/constants/forms' + +const serverValidate = createServerValidate({ + ...formOpts, + onServerValidate: yourSchemaHere, +}) + +export const getFormDataFromServer = createServerFn({ method: 'GET' }).handler( + async () => { + return getFormData() + } +) +``` + +> 这个代码示例省略了一些相关代码以保持简洁。[有关我们的 SSR 集成的更多详细信息,请查看我们的文档。](/form/latest/docs/framework/react/guides/ssr) + +就这样,完全相同的验证逻辑在你的前端和后端都在运行。即使用户的浏览器禁用了 JavaScript,你的表单也会显示错误! + +# 展望未来 + +然而,我们不会止步于此 - 现在我们已经稳定,我们计划为 v1 添加新功能。这些功能包括: + +- [持久化 API](https://github.com/TanStack/form/pull/561) +- [Svelte 5 适配器](https://github.com/TanStack/form/issues/516) +- [更好的提交时值转换 DX](https://github.com/TanStack/form/issues/418) +- [表单组](https://github.com/TanStack/form/issues/419) + +以及更多。 + +# 感谢**你们** + +有太多人我想感谢,一旦开始就永远说不完。相反,我会向每个我想感谢的群体致意。 + +- 感谢我们的贡献者:如此多的人必须聚在一起才能实现这一切。从其他 TanStack 项目的维护者给予我们指导,到临时的 PR;这一切都帮助我们跨越了终点线。 + +- 感谢我们的早期采用者:那些冒险尝试我们并为我们的 API 和功能提供宝贵反馈的人。 +- 感谢报道我们工具的内容创作者:你们为我们的项目带来了更多关注 - 通过教育和反馈使它变得更好。 +- 感谢更广泛的社区:你们使用我们工具的热情极大地推动了团队。 + +最后,感谢**你**花时间阅读和探索我们最新的工具。❤️ diff --git a/app/blog/zh-hans/announcing-tanstack-query-v4.md b/app/blog/zh-hans/announcing-tanstack-query-v4.md new file mode 100644 index 000000000..a38232ec9 --- /dev/null +++ b/app/blog/zh-hans/announcing-tanstack-query-v4.md @@ -0,0 +1,59 @@ +--- +title: 宣布 TanStack Query v4 发布 +published: 2022-07-14 +authors: + - Dominik Dorfmeister +--- + +我们很高兴地宣布 [TanStack Query](/query/v4)(之前称为 `react-query`)的下一个版本 🎉。 +重新品牌化和重组为 monorepo 现在终于让我们能够将 `react-query` 的乐趣带给其他框架,如 `vue`、`svelte` 或 `solid`。 + +TanStack Query 建立在一个框架无关的核心之上,顶层有特定框架的适配器。这使我们能够在框架之间共享使 TanStack Query 出色的核心逻辑,如 QueryClient 或 Query Subscriptions,同时在适配器中也有特定框架的代码,如钩子(useQuery 和 useMutation)。 + +## 如何安装 + +``` +npm i @tanstack/react-query +# 或 +yarn add @tanstack/react-query +``` + +## 新特性 + +### 适当的离线支持 + +v4 已经将 TanStack Query 从一个数据获取库发展成为一个真正的异步状态管理器。之前关于必须有活动网络连接的所有假设现在都消失了,所以 TanStack Query *真正地*可以处理任何 Promise,无论你如何产生它。 +为了实现这一点,我们引入了全新的 [Network Mode](/query/v4/docs/guides/network-mode),帮助 TanStack Query 决定查询何时应该考虑在线状态。 + +### 稳定的持久化器 + +从 v3 开始,持久化器就作为一个实验性功能存在。它们允许你将 Query Cache 同步到外部位置(例如 localStorage)以供以后使用。在获得大量反馈后,我们重新设计并改进了 API,现在我们提供两个主要的持久化器: + +- SyncStoragePersister +- AsyncStoragePersister + +这些持久化器对大多数用例都工作得很好,但没有什么能阻止你编写自己的持久化器 - 只要它遵守所需的接口。 + +### 支持 React 18 + +React 18 在今年早些时候发布,v4 现在对它和它带来的新并发特性提供一流支持。为了实现这一点,内部订阅被重写以利用新的 `useSyncExternalStore` 钩子。 + +### 默认启用追踪查询 + +追踪查询是在 v3.6.0 中作为可选功能添加的性能优化。这个优化现在在 v4 中是默认行为,这应该会给你的渲染性能带来不错的提升。 + +### 简化的 API + +随着时间的推移,我们的一些 API 变得相当复杂,以至于它们相互矛盾。一些例子包括: + +- QueryKeys 在暴露时有时被转换为数组,有时不转换。 +- Query Filters 不直观且互斥。 +- 参数的默认值在不同方法上默认为相反的值 + +我们清理了许多这些不一致之处,以使开发者体验更好。v4 还提供了一个代码转换工具来帮助你进行迁移。 + +## 接下来是什么? + +首先是清理文档。正如你可能注意到的,它们仍然非常特定于 react,并且时不时地引用 `react-query`。请在我们重组文档时耐心等待,PR 总是受欢迎的。 + +此外,还有更多的适配器。目前,只有 React 适配器存在,但我们渴望很快添加更多框架。 diff --git a/app/blog/zh-hans/announcing-tanstack-query-v5.md b/app/blog/zh-hans/announcing-tanstack-query-v5.md new file mode 100644 index 000000000..5f68d1225 --- /dev/null +++ b/app/blog/zh-hans/announcing-tanstack-query-v5.md @@ -0,0 +1,62 @@ +--- +title: 宣布 TanStack Query v5 发布 +published: 2023-10-17 +authors: + - Dominik Dorfmeister +--- + +大约一年前,我们宣布了 [TanStack Query v5 路线图](https://github.com/TanStack/query/discussions/4252),从那时起整个团队一直在努力开发这个版本。所以我们非常高兴地宣布今天就是这一天:经过 91 个 alpha 版本、35 个 beta 版本和 16 个候选版本,TanStack Query [v5.0.0](https://github.com/TanStack/query/releases/tag/v5.0.0) 终于来了!🎉 + +v5 延续了 v4 的旅程,致力于使 TanStack Query 更小(v5 比 v4 小约 20%)、更好且更直观易用。这个版本的主要关注点之一是简化和精简我们提供的 API: + +## 重大变更 + +作为一个重大变更,我们从代码库中移除了大多数重载,统一了使用 `useQuery` 和其他钩子的方式。这是我们想在 v4 中完成的事情,但由于 [TypeScript 的限制](https://github.com/microsoft/TypeScript/issues/43371)而无法实现。TypeScript 在 TS 4.7 中解决了这个问题,因此我们能够移除所有用于不同参数数量调用 `useQuery` 的重载。这是一个巨大的开发体验提升,因为带有重载的方法通常会有相当糟糕的 TypeScript 错误信息。 + +这是 v5 中最大的突破性变化,但我们认为这是值得的。API 现在更加一致 - 你总是只需要传递*一个*对象。为了减轻手动更改所有出现位置的痛苦,我们在过去几个月里试图为这个即将到来的变化做好准备。文档已经更改为使用新的 API,我们在 eslint 包中发布了一个可自动修复的 [eslint 规则](/query/v4/docs/eslint/prefer-query-object-syntax)。此外,v5 还提供了 [代码转换工具](/query/v5/docs/react/guides/migrating-to-v5#codemod) 来帮助过渡。 + +除此之外,我们将 `cacheTime` 重命名为 `gcTime` 以更好地反映其功能,合并了 `keepPreviousData` 和 `placeholderData`,将 `loading` 状态重命名为 `pending`,并 [移除了 `useQuery` 中的回调](https://github.com/TanStack/query/discussions/5279)。所有这些变更使 v5 成为对新手最一致和最好的版本。 + +要了解更多关于重大变更的信息,请查看我们的 [迁移指南](/query/v5/docs/react/guides/migrating-to-v5)。 + +## 新特性 + +当然,v5 也带来了令人惊叹的新特性 🚀: + +### 简化的乐观更新 + +通过利用 `useMutation` 返回的 `variables`,享受全新的、简化的乐观更新方式,无需手动编写更新缓存的代码。更多详情,请查看 [乐观更新文档](/query/v5/docs/react/guides/optimistic-updates) + +### 可共享的 mutation 状态 + +一个经常被请求的特性,如这个 [两年前的 issue](https://github.com/TanStack/query/issues/2304) 所示,终于在 v5 中实现:你现在可以通过新的 [useMutationState](/query/v5/docs/react/reference/useMutationState) 钩子访问所有 mutations 的状态,这些状态可以在组件之间共享。 + +### 一流的 `suspense` 支持 + +没错 - 用于数据获取的 `suspense` 不再是实验性的,而是完全支持的。React Query 提供了新的 `useSuspenseQuery`、`useSuspenseInfiniteQuery` 和 `useSuspenseQueries` 钩子。查看 [suspense 文档](/query/v5/docs/react/guides/suspense) 了解它们与非 suspense 版本的区别。 + +#### 使用 React Server Components 进行流式传输 + +v5 还为 nextJs 中的服务器端 suspense 提供了实验性集成,统一了两个世界的精华:[react-query-next-experimental](/query/v5/docs/react/guides/advanced-ssr#experimental-streaming-without-prefetching-in-nextjs) 适配器允许我们编写单个 `useSuspenseQuery`,它将尽可能早地开始数据获取:在服务器上,在 SSR 期间。然后它会将结果流式传输到客户端,在那里它会自动放入缓存中,为我们提供 React Query 的所有交互性和数据同步功能。 + +### 改进的无限查询 + +无限查询现在可以 [一次预取多个页面](/query/v5/docs/react/guides/prefetching),你还可以指定缓存中存储的 [最大页面数量](/query/v5/docs/react/guides/infinite-queries#what-if-i-want-to-limit-the-number-of-pages)。 + +### 新的开发工具 + +Query 开发工具已经从头开始以框架无关的方式重写,使其可用于所有适配器。它们还进行了 UI 改造,并添加了一些新功能,如缓存内联编辑和亮色模式。 + +### 细粒度持久化 + +另一个来自 [2021 年的长期讨论](https://github.com/TanStack/query/discussions/2649) 强调了细粒度持久化和即时恢复功能的重要性(特别是对于移动开发),这是 `PersistQueryClient` 插件所不具备的。在 v5 中,我们现在有了一个新的 [experimental_createPersister](/query/v5/docs/react/plugins/createPersister) 插件,允许你单独持久化查询。 + +### `queryOptions` API + +现在我们有了统一的方式来调用 `useQuery`(只使用一个对象作为参数),我们也可以在此基础上构建更好的抽象。新的 [queryOptions](/query/v5/docs/react/typescript#typing-query-options) 函数为我们提供了一种类型安全的方式来在 `useQuery` 和命令式方法(如 `queryClient.prefetchQuery`)之间共享查询定义。此外,它还可以使 `queryClient.getQueryData` 类型安全。 + +--- + +## 感谢 + +我们希望你能像我们享受构建 v5 一样享受使用它。最后我们要感谢每一个让这个版本成为可能的人。无论你是核心贡献者,实现了路线图中的问题,修复了文档中的错别字,还是对 alpha 版本提供了反馈:每一个贡献都很重要!是人们让这个库变得伟大,我们很幸运能有这样一个令人惊叹的社区。❤️ diff --git a/app/blog/zh-hans/netlify-partnership.md b/app/blog/zh-hans/netlify-partnership.md new file mode 100644 index 000000000..8d3bc3bf5 --- /dev/null +++ b/app/blog/zh-hans/netlify-partnership.md @@ -0,0 +1,46 @@ +--- +title: TanStack + Netlify 合作伙伴关系 +published: 2025-03-18 +authors: + - Tanner Linsley +--- + +![Netlify 头图](/blog-assets/netlify-partnership/header.jpg) + +我们很高兴地宣布,**Netlify** 现在是 **TanStack Start** 的**官方部署合作伙伴**!我们携手合作,让开发者能够更轻松地构建和部署现代、类型安全和以用户为中心的 Web 应用程序。 + +Netlify 已经赢得了现代 Web 开发者终极部署平台的声誉。它对速度、简单性、模块化和灵活性的关注与 TanStack Start 对全栈开发的愿景完美契合。以下是 Netlify 的突出优势: + +- **零配置简单性** – 几秒钟内部署你的 TanStack Start 应用,无需繁琐设置。 +- **无服务器能力** – Netlify Functions 轻松实现动态、实时功能。 +- **全球性能** – 通过 Netlify 的全球边缘网络提供快速、可靠的应用。 +- **开发者优先工具** – 即时预览、自动化工作流和无缝集成让构建变得愉悦。 + +## 为什么选择 Netlify? + +Netlify 不仅仅是一个部署提供商—他们与我们密切合作,确保部署 TanStack Start 应用程序不仅快速,而且针对最佳开发者体验进行了优化。无论你是构建交互式 UI、数据密集型仪表板、实时工具还是 AI 驱动的应用程序,Netlify 的平台都能让这个过程变得无缝。 + +作为此次合作的一部分,Netlify 还推出了一个**全栈 AI 聊天机器人启动模板**,展示了 TanStack Start 强大的数据管理能力以及 Netlify Functions。这个模板提供: + +- 使用 TanStack Query 进行**实时数据处理** +- 使用 TanStack Router 进行**高效路由** +- 与 Netlify 进行**无缝服务器函数集成** +- **生产就绪的部署配置** + +要开始使用模板,只需运行: + +``` +npx create-tsrouter-app@latest --template file-router --add-ons tanchat +``` + +这个模板是探索 TanStack Start 和 Netlify 如何协同工作以简化现代 Web 开发的绝佳方式。 + +## 未来展望 + +我们才刚刚开始。期待 TanStack Start 和 Netlify 之间更多的更新、新功能和更深入的合作。敬请关注成功案例、指南和展示这个强大组合可能性的真实案例。 + +此外,3 月 31 日加入我们参加 [Netlify 的 Remote Desk 系列](https://www.netlify.com/webinars/netlify-remote-desk/) 的**特别 TanStack Start 专题**。我们将深入探讨现场演示、开发者技巧,并进行独家问答环节,展示如何在 Netlify 上释放 TanStack Start 的全部潜力。 + +**准备好开始了吗?** 查看 [TanStack Start 文档](/start/latest/docs/framework/react/overview),探索部署指南,今天就开始使用 Netlify 构建吧。 + +非常感谢 Netlify 对开发者的支持和赋能。让我们一起构建令人惊叹的东西!🚀 diff --git a/app/blog/zh-hans/tanstack-router-typescript-performance.md b/app/blog/zh-hans/tanstack-router-typescript-performance.md new file mode 100644 index 000000000..b387ba7da --- /dev/null +++ b/app/blog/zh-hans/tanstack-router-typescript-performance.md @@ -0,0 +1,241 @@ +--- +title: TanStack Router 在 TypeScript 性能方面的里程碑 +published: 2024-09-17 +authors: + - Christopher Horobin +--- + +TanStack Router 在类型安全路由方面不断突破边界。 + +路由器的组件如 `` 及其钩子如 `useSearch`、`useParams`、`useRouteContext` 和 `useLoaderData`,从路由定义中推断以提供出色的类型安全。使用 TanStack Router 的应用程序通常会在其路由定义中使用带有复杂类型的外部依赖,用于 `validateSearch`、`context`、`beforeLoad` 和 `loader`。 + +虽然开发体验很好,但当路由定义累积成路由树并变得庞大时,编辑器体验可能会开始变慢。我们对 TanStack Router 进行了许多 TypeScript 性能改进,使得只有在推断复杂性变得非常大时才会出现问题。我们密切关注诊断信息,如实例化,并试图减少 TypeScript 对每个单独路由定义进行类型检查所需的时间。 + +尽管所有这些过去的努力(确实有帮助),我们不得不解决房间里的大象。TanStack Router 中实现出色编辑器体验的根本问题不一定与整体 typescript 检查时间有关。我们一直在努力解决的问题是 TypeScript 语言服务在类型检查累积的路由树时的瓶颈。对于熟悉 TypeScript 跟踪的人来说,大型 TanStack Router 应用程序的跟踪可能看起来类似于以下内容: + +![显示路由树被推断的跟踪](/blog-assets/tsr-perf-milestone/tracing-slow.png) + +对于不了解的人来说,你可以使用以下命令从 TypeScript 生成跟踪: + +``` +tsc --generatetrace trace +``` + +这个例子有 400 个路由定义,所有的 `validateSearch` 都使用 `zod` 并通过路由的 `context` 和 `loader` 集成 TanStack Query - 这是一个极端的例子。跟踪开始时的大块就是 TypeScript 在首次遇到 `` 组件实例时进行类型检查的内容。 + +语言服务器的工作原理是从头开始对文件(或文件的一个区域)进行类型检查,但仅限于该文件/区域。这意味着每当你与 `` 组件的实例交互时,语言服务都必须执行这项工作。事实证明,这就是我们在从累积的路由树推断所有必要类型时遇到的瓶颈。如前所述,路由定义本身可能包含来自外部验证库的复杂类型,这些类型也需要推断。 + +很早就显而易见,这显然会降低编辑器体验。 + +## 为语言服务分解工作 + +理想情况下,语言服务应该只需要根据 `` 导航 `to` 的位置从路由定义中进行推断,而不是必须遍历整个路由树。这样,语言服务就不需要忙于推断不是导航目标的路由定义的类型。 + +不幸的是,基于代码的路由树依赖推断来构建路由树,这会触发上面跟踪中显示的大块。然而,TanStack Router 的基于文件的路由,会在创建或修改路由时自动生成路由树。这意味着我们可以在这里进行一些探索,看看是否能获得更好的性能。 + +之前即使是基于文件的路由,路由树也是像下面这样创建的: + +```tsx +export const routeTree = rootRoute.addChildren({ + IndexRoute, + LayoutRoute: LayoutRoute.addChildren({ + LayoutLayout2Route: LayoutLayout2Route.addChildren({ + LayoutLayout2LayoutARoute, + LayoutLayout2LayoutBRoute, + }), + }), + PostsRoute: PostsRoute.addChildren({ PostsPostIdRoute, PostsIndexRoute }), +}) +``` + +生成路由树是减少路由树繁琐配置但保持推断在重要位置的结果。这就是引入第一个重要变更以提高编辑器性能的地方。我们可以利用这个生成步骤来*声明路由树*,而不是推断路由树。 + +```tsx +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + PostsRoute: typeof PostsRouteWithChildren +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + PostsRoute: PostsRouteWithChildren, +} + +export const routeTree = rootRoute._addFileChildren(rootRouteChildren) +``` + +注意使用 `interface` 来声明组成路由树的子路由。在生成路由树时,这个过程会对所有路由及其子路由重复进行。有了这个变更,运行跟踪让我们更好地了解语言服务内部发生的事情。 + +![显示路由树被声明的跟踪](/blog-assets/tsr-perf-milestone/tracing-declare-route-tree.png) + +这仍然很慢,我们还没有完全达到目标,但有一些变化 - _跟踪不同了_。整个路由树的类型推断仍在发生,但现在是在*其他地方*进行的。在处理我们的类型时,发现它发生在一个名为 `ParseRoute` 的类型中。 + +```tsx +export type ParseRoute = TRouteTree extends { + types: { children: infer TChildren } +} + ? unknown extends TChildren + ? TAcc + : TChildren extends ReadonlyArray + ? ParseRoute + : ParseRoute + : TAcc +``` + +这个类型遍历路由树以创建所有路由的联合。这个联合反过来用于创建从 `id` -> `Route`、`from` -> `Route` 和 `to` -> `Route` 的类型映射。这个映射的一个例子存在于映射类型中。 + +```tsx +export type RoutesByPath = { + [K in ParseRoute as K['fullPath']]: K +} +``` + +这里的重要认识是,当使用基于文件的路由时,我们可以通过在生成路由树时自己输出该映射类型来完全跳过 `ParseRoute` 类型。相反,我们可以生成以下内容: + +```tsx +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/posts': typeof PostsRouteWithChildren + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts': typeof PostsIndexRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/posts': typeof PostsRouteWithChildren + '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute + '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute + '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/posts' + | '/posts/$postId' + | '/posts/' + | '/layout-a' + | '/layout-b' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/posts/$postId' | '/posts' | '/layout-a' | '/layout-b' + id: + | '__root__' + | '/' + | '/_layout' + | '/posts' + | '/_layout/_layout-2' + | '/posts/$postId' + | '/posts/' + | '/_layout/_layout-2/layout-a' + | '/_layout/_layout-2/layout-b' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + PostsRoute: typeof PostsRouteWithChildren +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + PostsRoute: PostsRouteWithChildren, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() +``` + +除了声明子路由外,我们还声明了将路径映射到路由的接口。 + +这个变更以及其他类型级别的变更(仅在未注册这些类型时才有条件地使用 `ParseRoute`)导致了一个跟踪,这正是我们一直以来的目标 🥳 + +![路由树声明被更快地推断的跟踪](/blog-assets/tsr-perf-milestone/tracing-faster.png) + +第一个引用 `` 的文件不再触发整个路由树的推断,这显著提高了感知到的语言服务速度。 + +通过这样做,TypeScript 将根据 `` 引用的特定路由推断所需的类型。当所有路由都被链接时,这可能不会转化为整体更好的 TypeScript 类型检查时间,但对于文件/区域中的语言服务来说,这是显著的速度提升。 + +两者之间的差异很明显,如下面这些具有复杂推断的大型路由树所示(本例中有 400 个): + +
+ + +
+ +你可能会认为这是*作弊*,因为我们在路由树生成阶段做了很多繁重的工作。我们对此的回应是,这个用于基于文件的路由(现在也包括虚拟基于文件的路由)的生成步骤已经存在,而且每当你修改或创建新路由时都是必要的步骤。 + +因此,一旦创建了路由并生成了路由树,路由定义中的所有内容的推断都保持不变。这意味着你可以对 `validateSearch`、`beforeLoad`、`loader` 等进行更改,推断的类型始终会立即反映出来。 + +开发体验没有改变,但在编辑器中的性能感觉很棒(特别是当你处理大型路由树时)。 + +## 基本规则 + +这个变更涉及许多 TanStack Router 的导出被改进,以使消费这些生成的类型更加高效,同时在使用基于代码的路由时仍能回退到整个路由树推断。我们的代码库中仍然有一些区域依赖于完整的路由树推断。这些区域是我们版本的松散/非严格模式。 + +```tsx + + + ({..prev, page: 0 })} /> +``` + +上面所有三种 `` 的用法都需要推断整个路由树,因此在与它们交互时会导致更差的编辑器体验。 + +在前两个实例中,TanStack Router 不知道你想导航到哪个路由,因此它尽最大努力从路由树中的所有路由推断出一个非常松散的类型。上面的第三个 `` 实例在 `search` 更新器函数中使用了 `prev` 参数,但在这种情况下,TanStack Router 不知道你是从哪个 `Route` 导航 `from`,因此它需要再次通过扫描整个路由树来猜测 `prev` 的松散类型。 + +在你的编辑器中,`` 最高性能的用法如下: + +```tsx + + + ({...prev, page: 0 })} /> +``` + +TanStack Router 可以在这些情况下将类型缩小到特定路由。这意味着随着应用程序的扩展,你获得更好的类型安全和更好的编辑器性能。因此,我们鼓励在这些情况下使用 `from` 和/或 `to`。需要明确的是,在第三个例子中,只有在使用 `prev` 参数时才需要使用 `from`,否则,TanStack Router 不需要推断整个路由树。 + +这些更松散的类型也出现在 `strict: false` 模式中。 + +```tsx +const search = useSearch({ strict: false }) +const params = useParams({ strict: false }) +const context = useRouteContext({ strict: false }) +const loaderData = useLoaderData({ strict: false }) +const match = useMatch({ strict: false }) +``` + +在这种情况下,通过使用推荐的 `from` 属性可以获得更好的编辑器性能和类型安全。 + +```tsx +const search = useSearch({ from: '/dashboard' }) +const params = useParams({ from: '/dashboard' }) +const context = useRouteContext({ from: '/dashboard' }) +const loaderData = useLoaderData({ from: '/dashboard' }) +const match = useMatch({ from: '/dashboard' }) +``` + +## 展望未来 + +展望未来,我们相信 TanStack Router 在类型安全和 TypeScript 性能之间取得了最佳平衡,而不必在基于文件(和虚拟基于文件)路由中使用的类型推断质量上做出妥协。路由定义中的所有内容都保持推断,生成的路由树中的变更只是通过在重要的地方声明必要的类型来帮助语言服务,这是你永远不会想自己编写的内容。 + +这种方法对语言服务来说也是可扩展的。我们能够创建数千个路由定义,只要你坚持使用 TanStack Router 的 `strict` 部分,语言服务就能保持稳定。 + +我们将继续改进 TanStack Router 的 TypeScript 性能,以减少整体检查时间并进一步改进语言服务性能,但仍然觉得这是一个重要的里程碑,值得分享,我们希望 TanStack Router 的用户会欣赏这一点。 diff --git a/app/blog/zh-hans/why-tanstack-start-and-router.md b/app/blog/zh-hans/why-tanstack-start-and-router.md new file mode 100644 index 000000000..bb9587972 --- /dev/null +++ b/app/blog/zh-hans/why-tanstack-start-and-router.md @@ -0,0 +1,74 @@ +--- +title: 为什么选择 TanStack Start 和 Router? +published: 2024-12-03 +authors: + - Tanner Linsley +--- + +![TanStack Start 和 Router 博客头图](/blog-assets/why-tanstack-start-and-router/tanstack-start-blog-header.jpg) + +构建现代 Web 应用程序绝非易事。我们选择的框架和工具不仅会影响开发体验,还会影响我们构建的应用程序的成功与否。虽然市面上有许多优秀的框架,但我认为 **TanStack Router** 和 **TanStack Start** 因其解决当今开发者面临的挑战的能力以及对未来的准备而脱颖而出。 + +这些不仅仅是另一套工具—它们代表着以更少的阻力和更多的乐趣构建更好应用程序的承诺。以下是我认为你会和我一样喜欢使用它们的原因。 + +### 可靠的类型安全 + +类型安全不仅仅是一个流行词—它是创建健壮、可维护应用程序的基础工具。TanStack Router 超越了基础,提供了**上下文类型安全**,类型可以无缝地流经应用程序的每个部分。路由定义、参数、导航,甚至状态管理都可以完全推断类型。 + +这对你意味着什么?这意味着不用再猜测参数是否正确定义,不用再调试类型不匹配的问题,也不需要额外的插件或 AST 转换来填补空白。TanStack Router 与 TypeScript 的自然架构协同工作,使体验流畅、可预测且令人愉悦。 + +这种级别的类型安全不仅节省时间—还能建立信心。我相信其他框架还需要花费数年时间才能赶上这一点。 + +### 释放 URL 状态管理的力量 + +Web 开发中最被忽视但最强大的工具之一就是 URL。它是最原始的状态管理系统—快速、可共享且直观。然而,许多框架将 URL 视为次要考虑,仅提供基本的读写状态工具。 + +TanStack Router 改变了这种思维。在 URL 中管理状态不仅仅是支持的—而是被鼓励的。通过直观的 API,你可以验证、读取和更新搜索参数,内置类型安全和运行时验证。想要创建深度嵌套的动态过滤系统或将应用程序状态与 URL 同步?这一切都变得轻而易举。 + +但这不仅仅是关于开发者便利性—这是关于创造更好的用户体验。当你的应用程序状态存在于 URL 中时,用户可以共享它、为它添加书签,并从他们离开的地方继续。TanStack Router 让这一切变得简单自然。 + +### 熟悉的模式,更多灵活性 + +如果你使用过 Remix 或 Next.js,你会在 TanStack Start 中找到许多熟悉的概念。但熟悉并不意味着妥协。我们采用了这些框架中的一些最佳理念并将其推进,去除了约束并引入了更多灵活性。 + +例如,如果你来自像 Remix 这样的服务器优先框架,路由模式和服务器函数集成会让你感到自然,但它们同样适用于传统的客户端 SPA。你不必选边站—你可以获得两个世界的精华,且更少权衡。 + +### 为未来而构建(并已经在拥抱它) + +Web 正在快速变化。随着 React Server Components (RSCs) 即将到来,React 19 引入新模式,以及流式传输成为数据传递的标准,框架需要做的不仅仅是跟上—它们需要引领。 + +TanStack Start 已经为未来做好准备。RSCs 被视为另一种服务器端状态,具有强大的原语用于缓存、失效和将它们组合到应用程序中。流式传输不是事后考虑—它被内置到 TanStack 工作方式的核心中,让你能够逐步向客户端发送数据和 HTML,而无需额外的复杂性。 + +但我们不仅仅是关注未来。TanStack Start 还让这些高级功能变得平易近人,现在就可以使用。你不需要等待"下一个重大事物"就可以开始构建感觉像未来的应用程序。 + +### SPA 并未消亡(我们只是让它们变得更好) + +现在有很多关于服务器优先架构的讨论,虽然它们令人兴奋,但这并不是全部。单页应用程序(SPA)仍然是构建快速、交互式应用程序的绝佳方式—特别是当做得对的时候。 + +TanStack Start 不仅保持了 SPA 的可行性—还让它们变得更好。通过简化的模式、强大的状态管理和深度集成,你可以构建更高性能、更易维护且使用愉悦的 SPA。无论你是服务器优先、客户端优先,还是介于两者之间,TanStack 都能为你提供构建所需应用程序的工具。 + +### 无与伦比的数据集成 + +如果你使用过 **TanStack Query**,你已经知道它多大程度上简化了数据获取。但 TanStack Query 和 TanStack Router 之间的集成才是真正的魔法所在。预取数据、缓存结果和流式更新都是无缝的、直观的,并且可以扩展。 + +例如,你可以在路由加载器中预取数据,将其流式传输到客户端,并按需水合—所有这些都使用单个 API。无论你是管理简单的博客还是复杂的仪表板,你会发现自己花在连接数据上的时间更少,花在构建功能上的时间更多。 + +这不仅仅是一个集成—这是路由和数据获取之间的伙伴关系,让其他一切相形见绌。 + +### 可扩展的路由 + +路由不仅仅是一个实用工具—它是每个应用程序的骨干。然而,当事情变得复杂时,大多数路由器都会遇到困难。这正是 TanStack Router 大放异彩的地方。它可以处理从几个简单路由到数千个深度嵌套路由,而不会有任何问题。 + +类型安全导航、层次路由上下文和高级状态同步等功能,使得构建在规模和复杂性上都可扩展的应用程序变得容易。而且因为 TanStack Router 原生支持 TypeScript,你可以获得所有类型安全的好处,而不会牺牲性能或灵活性。 + +### 持续创新 + +让我最兴奋的是 TanStack 才刚刚起步。从同构服务器函数到强大的缓存原语和简化的 React Server Component 支持,我们不断推动可能性的边界。我们的目标不仅仅是构建优秀的工具—而是构建帮助*你*构建更好应用程序的工具。 + +### 满足于"足够好"? + +其他框架都有其优势,但如果你在寻找创新、灵活且深度集成的工具,TanStack Router 和 Start 是独一无二的。它们不仅解决今天的问题—还帮助你构建为明天做好准备的应用程序。 + +那么为什么要等待?今天就探索 [TanStack Router](https://tanstack.dev/router) 和 [TanStack Start](https://tanstack.dev/start),看看应用程序开发可以有多好。 + +让我们一起构建令人惊叹的东西! diff --git a/app/blog/zh-hans/why-tanstack-start-is-ditching-adapters.md b/app/blog/zh-hans/why-tanstack-start-is-ditching-adapters.md new file mode 100644 index 000000000..b36dd0910 --- /dev/null +++ b/app/blog/zh-hans/why-tanstack-start-is-ditching-adapters.md @@ -0,0 +1,88 @@ +--- +title: 为什么 TanStack Start 要放弃适配器 +published: 2024-11-22 +authors: + - Tanner Linsley +--- + +![Nitro 头图](/blog-assets/why-tanstack-start-is-ditching-adapters/nitro.jpg) + +## 要不要"适配器"? + +构建一个新的前端 Javascript 框架是一项艰巨的任务,正如我在构建 TanStack Start(我的新 TanStack 驱动的全栈框架)时所学到的。有太多需要考虑的部分: + +- 路由 +- 服务器端渲染 +- RPCs 和 APIs +- 开发工具 +- **部署和托管** + +最后一个,**部署和托管**尤其棘手,因为现在似乎每个云环境都有自己独特的方式来让事情"恰到好处"地工作。当面对这个 TanStack Start 的决定时,我显然知道我想要首先支持哪些主机,Vercel 就在这个列表的顶部。 + +我的第一反应是开始构建一个可以为每个主机提供"适配器"的系统,然后先专注于编写 Vercel 适配器。 + +然而,这个计划从一开始就有缺陷。很快我就意识到,我个人将负责(至少在开始时)编写大部分(如果不是全部)代码,使 TanStack Start 不仅能在 Vercel 上运行,还要在许多其他目标和平台上运行。经过一些快速研究,仅这一任务就令人望而生畏,让我质疑自己构建服务器绑定的 JS 框架的动机。 + +从技术上讲,仅仅通过遵守一些输出文件/目录命名约定,部署到 Vercel 的工作已经非常简单。然而,令人感到困扰的是需要支持的环境/主机数量之多。真的很多!看看 [Remix 不断增长的服务器适配器列表](https://remix.run/docs/en/main/other-api/adapter)就知道了!而且不只是 Remix 有这样的列表。大多数服务器绑定的 JS 框架都有类似的东西。我基本上是在承诺在框架的最初几个月就要编写至少 10 个适配器,而我几乎还没有涉及框架本身的令人兴奋的功能(更不用说维护和更新这些适配器的工作)。 + +这里的残酷现实是**没有办法绕过这个问题。**如果你的框架在提供**任何面向服务器的代码,你需要确保它可以在任何可以部署的地方完美运行。** + +所以,就在我即将屈服于编写上百个适配器并在余生处理上游破坏性变更的无限悲伤时,我与我的朋友 [Ryan Carniato](https://twitter.com/ryancarniato) 讨论了 Solid JS 如何通过我们的表亲框架 [Solid Start](https://start.solidjs.com/) 来解决这个问题,他自信地说"直接使用 Vite 和 Nitro"。 + +## TanStack Start = Nitro + Vite + TanStack Router + +[Nitro](https://nitro.unjs.io/) 是一个"用于构建带有自己观点的服务器端框架的 JavaScript 工具包",由 [Vite](https://vite.dev/) 驱动。那么是什么让它如此特别呢? + +Nitro 有很多令人惊叹的功能,使其对构建框架非常有用,但最酷的部分之一是它由 H3 和 Vite 驱动。Nitro 的标语就是"创建具有所需一切的 Web 服务器,并**将它们部署到你喜欢的任何地方**"(强调是我加的)。 + +简单来说,Nitro 有效地使 TanStack Start 变得"_无适配器_"。它使用 H3,这是一个维护自己的低级适配器的 HTTP 框架,这样你就可以编写一次服务器代码并在任何地方使用它(听起来很像 React!)。 + +通过使用 Nitro,TanStack Start 的所有适配器问题都消失了。我甚至不需要考虑它们! + +事实上,部署到 Vercel 比我最初计划的还要简单:只需要将 `vercel` 目标传递给我们的 `defineConfig` 的 `server.preset` 选项,这个选项会传递给 Nitro: + +```jsx +import { defineConfig } from '@tanstack/start/config' + +export default defineConfig({ + server: { + preset: 'vercel', + }, +}) +``` + +## 它支持什么? + +Nitro、H3 和 Vite **令人印象深刻,这是最起码的评价。**我们很高兴看到在第一次尝试时,大量 Vercel 功能都完美地开箱即用,包括 GitHub 集成、服务器函数、边缘函数/中间件、不可变部署、环境变量、服务器端渲染,甚至是流式传输。 + +这是一个庞大的列表,我们基本上通过使用 Nitro 和 Vite 就免费获得了。 + +## TanStack Start 即将到来! + +有了解决构建和部署的问题,并内置支持将我的 GitHub 仓库直接集成到我最喜欢的托管提供商,我终于可以专注于我认为使 TanStack Start 特别的地方: + +- 一个同类最佳的完全类型安全的路由器 +- 用于构建服务器绑定 RPCs 的灵活原语 +- 可选的服务器功能(SSR、APIs、RSCs 等) +- 与其他 TanStack 原语(如 TanStack Query)的深度集成 +- 以及更多即将到来的功能! + +## 更进一步 + +能够将这么多工作交给 Nitro 和 Vite 并获得这么多令人惊叹的功能是很棒的,但这绝对不是使用托管平台*每个*功能的 100% 完整解决方案,特别是 Vercel,我们在那里可以访问的不仅仅是部署。我们也一直在思考更多功能,比如 [边缘网络缓存](https://vercel.com/docs/edge-network/caching) 和我个人最喜欢的 [_偏差保护_](https://vercel.com/docs/deployments/skew-protection)。 + +例如,偏差保护(确保客户端和服务器在各自的部署中保持同步)需要的不仅仅是构建步骤。它还涉及在运行时将平台原语深度集成到框架中的能力,或者在特定情况下,能够将特定的 cookie 或头部注入到指向 Vercel 的传出 API/服务器请求中。 + +我很高兴地报告,TanStack Start 将提供令人惊叹的强大中间件原语(用于 API 路由和服务器函数 RPCs),这将使其成为一行代码的事情,甚至可能是自动的(希望如此)。 + +这种级别的开发者体验和集成让我对未来感到兴奋,我相信这就是开源的真正意义:将生态系统中强大的工具组合在一起,为开发者和用户提供令人惊叹的体验。 + +我想不出比 TanStack Start、Nitro、Vite 和 Vercel 更好的组合来为你提供同类最佳的 Web 应用体验。 + +## 60 秒内试用 + +TanStack Start 目前处于 Beta 阶段!点击下面的 ["Deploy"](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ftanstack%2Frouter%2Ftree%2Fmain%2Fexamples%2Freact%2Fbasic-file-based&project-name=my-tanstack-project&repository-name=my-tanstack-project) 按钮,在约 1 分钟内创建并部署一个全新的 TanStack Start "Basic" 模板到 Vercel。 + +[![使用 Vercel 部署](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ftanstack%2Frouter%2Ftree%2Fmain%2Fexamples%2Freact%2Fbasic-file-based&project-name=my-tanstack-project&repository-name=my-tanstack-project) + +我们希望你喜欢我们正在构建的东西,迫不及待地想要得到你的反馈! diff --git a/app/client.tsx b/app/client.tsx new file mode 100644 index 000000000..41f74b4a3 --- /dev/null +++ b/app/client.tsx @@ -0,0 +1,9 @@ +/// +import { hydrateRoot } from 'react-dom/client' +import { StartClient } from '@tanstack/start' +import { createRouter } from './router' +import './utils/sentry' + +const router = createRouter() + +hydrateRoot(document, ) diff --git a/app/components/BackgroundAnimation.tsx b/app/components/BackgroundAnimation.tsx new file mode 100644 index 000000000..29037a28f --- /dev/null +++ b/app/components/BackgroundAnimation.tsx @@ -0,0 +1,231 @@ +import * as React from 'react' +import { usePrefersReducedMotion } from '~/utils/usePrefersReducedMotion' +import { twMerge } from 'tailwind-merge' +import { useMounted } from '~/hooks/useMounted' +import { useRouterState } from '@tanstack/react-router' + +export function BackgroundAnimation() { + const canvasRef = React.useRef(null) + const prefersReducedMotion = usePrefersReducedMotion() + const mounted = useMounted() + const isHomePage = useRouterState({ + select: (s) => s.location.pathname === '/', + }) + + React.useEffect(() => { + if (prefersReducedMotion !== false) { + return + } + + const canvas = canvasRef.current + + let morphDuration = 2000 + const waitDuration = 1000 * 60 * 2 + + const easingFn = cubicBezier(0.645, 0.045, 0.355, 1.0) + + if (canvas) { + const ctx = canvas.getContext('2d')! + + let rafId: ReturnType | null = null + let timeout: ReturnType | null = null + let startTime = performance.now() + + function createBlobs() { + return shuffle([ + { + color: { h: 10, s: 100, l: 50 }, + }, + { + color: { h: 40, s: 100, l: 50 }, + }, + { + color: { h: 150, s: 100, l: 50 }, + }, + { + color: { h: 200, s: 100, l: 50 }, + }, + ]).map((blob) => ({ + ...blob, + x: Math.random() * canvas!.width, + y: Math.random() * canvas!.height, + r: Math.random() * 500 + 700, + colorH: blob.color.h, + colorS: blob.color.s, + colorL: blob.color.l, + })) + } + + function shuffle(array: T[]) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[array[i], array[j]] = [array[j], array[i]] + } + return array + } + + let startBlobs = createBlobs() + let currentBlobs = startBlobs + let targetBlobs: ReturnType = [] + + function resizeHandler() { + // Create an offscreen canvas and copy the current content + const offscreen = document.createElement('canvas') + offscreen.width = canvas!.width + offscreen.height = canvas!.height + offscreen.getContext('2d')!.drawImage(canvas!, 0, 0) + + // Resize the main canvas + canvas!.width = window.innerWidth + canvas!.height = window.innerHeight + + // Stretch and redraw the saved content to fill the new size + ctx.drawImage(offscreen, 0, 0, canvas!.width, canvas!.height) + } + + function start() { + if (timeout) { + clearTimeout(timeout) + } + if (rafId) { + cancelAnimationFrame(rafId) + } + + startBlobs = JSON.parse(JSON.stringify(currentBlobs)) + targetBlobs = createBlobs() + startTime = performance.now() + animate() + } + + function animate() { + ctx.clearRect(0, 0, canvas!.width, canvas!.height) + + const time = performance.now() - startTime + const progress = time / morphDuration + const easedProgress = easingFn(progress) + + // Draw the blobs + startBlobs.forEach((startBlob, i) => { + const targetBlob = targetBlobs[i] + + currentBlobs[i].x = interpolate( + startBlob.x, + targetBlob.x, + easedProgress + ) + currentBlobs[i].y = interpolate( + startBlob.y, + targetBlob.y, + easedProgress + ) + + const gradient = ctx.createRadialGradient( + currentBlobs[i].x, + currentBlobs[i].y, + 0, + currentBlobs[i].x, + currentBlobs[i].y, + currentBlobs[i].r + ) + + currentBlobs[i].colorH = interpolate( + startBlob.colorH, + targetBlob.colorH, + easedProgress + ) + currentBlobs[i].colorS = interpolate( + startBlob.colorS, + targetBlob.colorS, + easedProgress + ) + currentBlobs[i].colorL = interpolate( + startBlob.colorL, + targetBlob.colorL, + easedProgress + ) + + gradient.addColorStop( + 0, + `hsla(${currentBlobs[i].colorH}, ${currentBlobs[i].colorS}%, ${currentBlobs[i].colorL}%, 1)` + ) + gradient.addColorStop( + 1, + `hsla(${currentBlobs[i].colorH}, ${currentBlobs[i].colorS}%, ${currentBlobs[i].colorL}%, 0)` + ) + + ctx.fillStyle = gradient + ctx.beginPath() + ctx.arc( + currentBlobs[i].x, + currentBlobs[i].y, + currentBlobs[i].r, + 0, + Math.PI * 2 + ) + ctx.fill() + }) + + if (progress < 1) { + rafId = requestAnimationFrame(animate) + } else { + timeout = setTimeout(() => { + morphDuration = 4000 + start() + }, waitDuration) + } + } + + resizeHandler() + start() + window.addEventListener('resize', resizeHandler) + + return () => { + if (rafId) { + cancelAnimationFrame(rafId) + } + if (timeout) { + clearTimeout(timeout) + } + window.removeEventListener('resize', resizeHandler) + } + } + }, [prefersReducedMotion]) + + return ( +
+ +
+ ) +} + +function cubicBezier(p1x: number, p1y: number, p2x: number, p2y: number) { + return function (t: number) { + const cx = 3 * p1x + const bx = 3 * (p2x - p1x) - cx + const ax = 1 - cx - bx + + const cy = 3 * p1y + const by = 3 * (p2y - p1y) - cy + const ay = 1 - cy - by + + const x = ((ax * t + bx) * t + cx) * t + const y = ((ay * t + by) * t + cy) * t + + return y + } +} + +function interpolate(start: number, end: number, progress: number) { + return start + (end - start) * progress +} diff --git a/app/components/BytesForm.tsx b/app/components/BytesForm.tsx new file mode 100644 index 000000000..ae12b2622 --- /dev/null +++ b/app/components/BytesForm.tsx @@ -0,0 +1,43 @@ +import useBytesSubmit from '~/components/useBytesSubmit' +import bytesImage from '~/images/bytes.svg' + +export default function BytesForm() { + const { state, handleSubmit, error } = useBytesSubmit() + if (state === 'submitted') { + return ( +

Success! Please, check your email to confirm your subscription.

+ ) + } + return ( +
+
+
+
+ Bytes +
+ +
+ +
+

+ No spam. Unsubscribe at any time. +

+ {error &&

{error}

} +
+ ) +} diff --git a/app/components/Carbon.tsx b/app/components/Carbon.tsx new file mode 100644 index 000000000..25f99957c --- /dev/null +++ b/app/components/Carbon.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' + +export function Carbon() { + const ref = React.useRef(null!) + + React.useEffect(() => { + ref.current.innerHTML = '' + const s = document.createElement('script') + s.id = '_carbonads_js' + s.src = `//cdn.carbonads.com/carbon.js?serve=CE7DEKQI&placement=react-tannerlinsleycom` + ref.current.appendChild(s) + }, []) + + return
+} diff --git a/app/components/CodeExplorer.tsx b/app/components/CodeExplorer.tsx new file mode 100644 index 000000000..c97fbf7c9 --- /dev/null +++ b/app/components/CodeExplorer.tsx @@ -0,0 +1,144 @@ +import React from 'react' +import { CodeBlock } from '~/components/Markdown' +import { FileExplorer } from './FileExplorer' +import { InteractiveSandbox } from './InteractiveSandbox' +import { CodeExplorerTopBar } from './CodeExplorerTopBar' +import type { GitHubFileNode } from '~/utils/documents.server' +import type { Library } from '~/libraries' + +function overrideExtension(ext: string | undefined) { + if (!ext) return 'txt' + + // Override some extensions + if (['cts', 'mts'].includes(ext)) return 'ts' + if (['cjs', 'mjs'].includes(ext)) return 'js' + if (['prettierrc', 'babelrc', 'webmanifest'].includes(ext)) return 'json' + if (['env', 'example'].includes(ext)) return 'sh' + if ( + [ + 'gitignore', + 'prettierignore', + 'log', + 'gitattributes', + 'editorconfig', + 'lock', + 'opts', + 'Dockerfile', + 'dockerignore', + 'npmrc', + 'nvmrc', + ].includes(ext) + ) + return 'txt' + + return ext +} + +interface CodeExplorerProps { + activeTab: 'code' | 'sandbox' + codeSandboxUrl: string + currentCode: string + currentPath: string + examplePath: string + githubContents: GitHubFileNode[] | undefined + library: Library + prefetchFileContent: (path: string) => void + setActiveTab: (tab: 'code' | 'sandbox') => void + setCurrentPath: (path: string) => void + stackBlitzUrl: string +} + +export function CodeExplorer({ + activeTab, + codeSandboxUrl, + currentCode, + currentPath, + examplePath, + githubContents, + library, + prefetchFileContent, + setActiveTab, + setCurrentPath, + stackBlitzUrl, +}: CodeExplorerProps) { + const [isFullScreen, setIsFullScreen] = React.useState(false) + const [isSidebarOpen, setIsSidebarOpen] = React.useState(true) + + // Add escape key handler + React.useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isFullScreen) { + setIsFullScreen(false) + } + } + window.addEventListener('keydown', handleEsc) + return () => window.removeEventListener('keydown', handleEsc) + }, [isFullScreen]) + + // Add sidebar close handler + React.useEffect(() => { + const handleCloseSidebar = () => { + setIsSidebarOpen(false) + } + window.addEventListener('closeSidebar', handleCloseSidebar) + return () => window.removeEventListener('closeSidebar', handleCloseSidebar) + }, []) + + return ( +
+ + +
+
+ +
+ + + {currentCode} + + +
+
+ +
+
+ ) +} diff --git a/app/components/CodeExplorerTopBar.tsx b/app/components/CodeExplorerTopBar.tsx new file mode 100644 index 000000000..b998ca205 --- /dev/null +++ b/app/components/CodeExplorerTopBar.tsx @@ -0,0 +1,92 @@ +import React from 'react' +import { FaExpand, FaCompress } from 'react-icons/fa' +import { CgMenuLeft } from 'react-icons/cg' + +interface CodeExplorerTopBarProps { + activeTab: 'code' | 'sandbox' + isFullScreen: boolean + isSidebarOpen: boolean + setActiveTab: (tab: 'code' | 'sandbox') => void + setIsFullScreen: React.Dispatch> + setIsSidebarOpen: (isOpen: boolean) => void +} + +export function CodeExplorerTopBar({ + activeTab, + isFullScreen, + isSidebarOpen, + setActiveTab, + setIsFullScreen, + setIsSidebarOpen, +}: CodeExplorerTopBarProps) { + return ( +
+
+ {activeTab === 'code' ? ( + + ) : ( +
+ +
+ )} + + +
+
+ +
+
+ ) +} diff --git a/app/components/CookieConsent.tsx b/app/components/CookieConsent.tsx new file mode 100644 index 000000000..1e9a94f56 --- /dev/null +++ b/app/components/CookieConsent.tsx @@ -0,0 +1,268 @@ +import { Link } from '@tanstack/react-router' +import { useEffect, useState } from 'react' + +declare global { + interface Window { + dataLayer: any[] + gtag: any + } +} + +const EU_COUNTRIES = [ + 'AT', + 'BE', + 'BG', + 'CZ', + 'DE', + 'DK', + 'EE', + 'ES', + 'FI', + 'FR', + 'GB', + 'GR', + 'HR', + 'HU', + 'IE', + 'IS', + 'IT', + 'LT', + 'LU', + 'LV', + 'MT', + 'NL', + 'NO', + 'PL', + 'PT', + 'RO', + 'SE', + 'SI', + 'SK', + 'CH', +] + +export default function CookieConsent() { + const [showBanner, setShowBanner] = useState(false) + const [showSettings, setShowSettings] = useState(false) + const consentSettings = + typeof document !== 'undefined' + ? JSON.parse(localStorage.getItem('cookie_consent') || '{}') + : { analytics: false, ads: false } + + useEffect(() => { + const checkLocationAndSetConsent = async () => { + // Only check location if no consent has been set yet + if (!consentSettings.analytics && !consentSettings.ads) { + try { + const response = await fetch( + 'https://www.cloudflare.com/cdn-cgi/trace' + ) + const data = await response.text() + const country = data.match(/loc=(\w+)/)?.[1] + const isEU = country ? EU_COUNTRIES.includes(country) : false + + if (isEU) { + // Set default denied consent for EU users + const euConsent = { analytics: false, ads: false } + localStorage.setItem('cookie_consent', JSON.stringify(euConsent)) + updateGTMConsent(euConsent) + setShowBanner(true) + } else { + // For non-EU users, set default accepted consent and don't show banner + const nonEuConsent = { analytics: true, ads: true } + localStorage.setItem('cookie_consent', JSON.stringify(nonEuConsent)) + updateGTMConsent(nonEuConsent) + setShowBanner(false) + } + } catch (error) { + console.error('Error checking location:', error) + setShowBanner(true) + } + } else { + updateGTMConsent(consentSettings) + } + } + + checkLocationAndSetConsent() + }, []) + + const updateGTMConsent = (settings: { analytics: boolean; ads: boolean }) => { + window.dataLayer = window.dataLayer || [] + window.dataLayer.push({ + event: 'cookie_consent', + consent: { + analytics_storage: settings.analytics ? 'granted' : 'denied', + ad_storage: settings.ads ? 'granted' : 'denied', + ad_personalization: settings.ads ? 'granted' : 'denied', + }, + }) + + if (typeof window.gtag === 'function') { + window.gtag('consent', 'update', { + analytics_storage: settings.analytics ? 'granted' : 'denied', + ad_storage: settings.ads ? 'granted' : 'denied', + ad_personalization: settings.ads ? 'granted' : 'denied', + }) + } + + if (settings.analytics || settings.ads) { + restoreGoogleScripts() + } else { + blockGoogleScripts() + } + } + + const acceptAllCookies = () => { + const consent = { analytics: true, ads: true } + localStorage.setItem('cookie_consent', JSON.stringify(consent)) + updateGTMConsent(consent) + setShowBanner(false) + } + + const rejectAllCookies = () => { + const consent = { analytics: false, ads: false } + localStorage.setItem('cookie_consent', JSON.stringify(consent)) + updateGTMConsent(consent) + setShowBanner(false) + } + + const openSettings = () => setShowSettings(true) + const closeSettings = () => setShowSettings(false) + + const blockGoogleScripts = () => { + document.querySelectorAll('script').forEach((script) => { + if ( + script.src?.includes('googletagmanager.com') || + script.textContent?.includes('gtag(') + ) { + script.remove() + } + }) + document.cookie = + '_ga=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.google.com' + document.cookie = + '_gid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.google.com' + } + + const restoreGoogleScripts = () => { + if (!document.querySelector("script[src*='googletagmanager.com']")) { + const script = document.createElement('script') + script.src = 'https://www.googletagmanager.com/gtag/js?id=GTM-5N57KQT4' + script.async = true + document.body.appendChild(script) + } + } + + return ( + <> + {showBanner && ( +
+ + 我们使用 Cookie 来支持网站功能、分析和广告 + + (这是 TanStack 开源软件能够永久免费的重要原因) + + 。详情请查看我们的{' '} + + 隐私政策 + {' '} + 。 + +
+ + + +
+
+ )} + + {showSettings && ( +
+
+

Cookie 设置

+
+ + +
+ +
+
+
+
+ )} + + ) +} diff --git a/app/components/CountdownTimer.tsx b/app/components/CountdownTimer.tsx new file mode 100644 index 000000000..1969e0576 --- /dev/null +++ b/app/components/CountdownTimer.tsx @@ -0,0 +1,92 @@ +import { Fragment, useEffect, useState } from 'react' + +interface CountdownProps { + targetDate: string // YYYY-MM-DD format +} + +interface TimeLeft { + days: number + hours: number + minutes: number + seconds: number +} + +function calculateTimeLeft(targetDate: string): TimeLeft { + const target = new Date(`${targetDate}T00:00:00-08:00`) + const now = new Date() + const difference = +target - +now + + if (difference <= 0) { + return { + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + } + } + + return { + days: Math.floor(difference / (1000 * 60 * 60 * 24)), + hours: Math.floor((difference / (1000 * 60 * 60)) % 24), + minutes: Math.floor((difference / 1000 / 60) % 60), + seconds: Math.floor((difference / 1000) % 60), + } +} + +const formatNumber = (number: number) => number.toString().padStart(2, '0') + +const Countdown: React.FC = ({ targetDate }) => { + const [timeLeft, setTimeLeft] = useState( + calculateTimeLeft(targetDate) + ) + + useEffect(() => { + const timer = setInterval(() => { + const newTimeLeft = calculateTimeLeft(targetDate) + setTimeLeft(newTimeLeft) + if ( + newTimeLeft.days === 0 && + newTimeLeft.hours === 0 && + newTimeLeft.minutes === 0 && + newTimeLeft.seconds === 0 + ) { + clearInterval(timer) + } + }, 1000) + + return () => clearInterval(timer) + }, [targetDate]) + + if ( + timeLeft.days === 0 && + timeLeft.hours === 0 && + timeLeft.minutes === 0 && + timeLeft.seconds === 0 + ) { + return null + } + + return ( +
+ {['days', 'hours', 'minutes', 'seconds'].map((unit, index) => ( + + {index > 0 && ( + : + )} + +
+ + {formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(0)} + + + {formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(1)} + +

{unit}

+
+
+ ))} +
+ ) +} + +export default Countdown diff --git a/app/components/CountdownTimerSmall.tsx b/app/components/CountdownTimerSmall.tsx new file mode 100644 index 000000000..8693b2cbc --- /dev/null +++ b/app/components/CountdownTimerSmall.tsx @@ -0,0 +1,83 @@ +import { Fragment, useEffect, useState } from 'react' + +interface CountdownProps { + targetDate: string // YYYY-MM-DD format +} + +interface TimeLeft { + days: number + hours: number + minutes: number +} + +function calculateTimeLeft(targetDate: string): TimeLeft { + const target = new Date(`${targetDate}T00:00:00-08:00`) + const now = new Date() + const difference = +target - +now + + if (difference <= 0) { + return { + days: 0, + hours: 0, + minutes: 0, + } + } + + return { + days: Math.floor(difference / (1000 * 60 * 60 * 24)), + hours: Math.floor((difference / (1000 * 60 * 60)) % 24), + minutes: Math.floor((difference / 1000 / 60) % 60), + } +} + +const formatNumber = (number: number) => number.toString().padStart(2, '0') + +const Countdown: React.FC = ({ targetDate }) => { + const [timeLeft, setTimeLeft] = useState( + calculateTimeLeft(targetDate) + ) + + useEffect(() => { + const timer = setInterval(() => { + const newTimeLeft = calculateTimeLeft(targetDate) + setTimeLeft(newTimeLeft) + if ( + newTimeLeft.days === 0 && + newTimeLeft.hours === 0 && + newTimeLeft.minutes === 0 + ) { + clearInterval(timer) + } + }, 1000) + + return () => clearInterval(timer) + }, [targetDate]) + + if (timeLeft.days === 0 && timeLeft.hours === 0 && timeLeft.minutes === 0) { + return null + } + + return ( +
+ {['days', 'hours', 'minutes'].map((unit, index) => ( + + {index > 0 && ( + : + )} + +
+ + {formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(0)} + + + {formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(1)} + +

{unit}

+
+
+ ))} +
+ ) +} + +export default Countdown diff --git a/app/components/DefaultCatchBoundary.tsx b/app/components/DefaultCatchBoundary.tsx new file mode 100644 index 000000000..1c5747f2f --- /dev/null +++ b/app/components/DefaultCatchBoundary.tsx @@ -0,0 +1,66 @@ +import { + ErrorComponent, + ErrorComponentProps, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/react-router' + +// type DefaultCatchBoundaryType = { +// status: number +// statusText: string +// data: string +// isRoot?: boolean +// } + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+

+ {/*
{status}
+ {statusText ? ( +
{statusText}
+ ) : null} */} +

+ +
+ + {isRoot ? ( + + TanStack Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/app/components/Doc.tsx b/app/components/Doc.tsx new file mode 100644 index 000000000..5615f5893 --- /dev/null +++ b/app/components/Doc.tsx @@ -0,0 +1,150 @@ +import * as React from 'react' +import { FaEdit } from 'react-icons/fa' +import { marked } from 'marked' +import markedAlert from 'marked-alert' +import { gfmHeadingId, getHeadingList } from 'marked-gfm-heading-id' +import { DocTitle } from '~/components/DocTitle' +import { Markdown } from '~/components/Markdown' +import { Toc } from './Toc' +import { twMerge } from 'tailwind-merge' +import { TocMobile } from './TocMobile' +import { GadLeader } from './GoogleScripts' + +type DocProps = { + title: string + content: string + repo: string + branch: string + filePath: string + shouldRenderToc?: boolean + colorFrom?: string + colorTo?: string +} + +export function Doc({ + title, + content, + repo, + branch, + filePath, + shouldRenderToc = false, + colorFrom, + colorTo, +}: DocProps) { + const { markup, headings } = React.useMemo(() => { + const markup = marked.use( + { gfm: true }, + gfmHeadingId(), + markedAlert() + )(content) as string + + const headings = getHeadingList() + + return { markup, headings } + }, [content]) + + const isTocVisible = shouldRenderToc && headings && headings.length > 1 + + const markdownContainerRef = React.useRef(null) + const [activeHeadings, setActiveHeadings] = React.useState>([]) + + const headingElementRefs = React.useRef< + Record + >({}) + + React.useEffect(() => { + const callback = (headingsList: Array) => { + headingElementRefs.current = headingsList.reduce( + (map, headingElement) => { + map[headingElement.target.id] = headingElement + return map + }, + headingElementRefs.current + ) + + const visibleHeadings: Array = [] + Object.keys(headingElementRefs.current).forEach((key) => { + const headingElement = headingElementRefs.current[key] + if (headingElement.isIntersecting) { + visibleHeadings.push(headingElement) + } + }) + + if (visibleHeadings.length >= 1) { + setActiveHeadings(visibleHeadings.map((h) => h.target.id)) + } + } + + const observer = new IntersectionObserver(callback, { + rootMargin: '0px', + threshold: 0.2, + }) + + const headingElements = Array.from( + markdownContainerRef.current?.querySelectorAll( + 'h2[id], h3[id], h4[id], h5[id], h6[id]' + ) ?? [] + ) + headingElements.forEach((el) => observer.observe(el)) + + return () => observer.disconnect() + }, []) + + return ( + + {shouldRenderToc ? : null} +
+
+ + {title ? {title} : null} +
+
+
+
+ +
+
+
+ +
+
+ + {isTocVisible && ( +
+ +
+ )} +
+ + ) +} diff --git a/app/components/DocContainer.tsx b/app/components/DocContainer.tsx new file mode 100644 index 000000000..10943b9f3 --- /dev/null +++ b/app/components/DocContainer.tsx @@ -0,0 +1,19 @@ +import { HTMLProps } from 'react' +import { twMerge } from 'tailwind-merge' + +export function DocContainer({ + children, + ...props +}: { children: React.ReactNode } & HTMLProps) { + return ( +
+ {children} +
+ ) +} diff --git a/src/components/DocTitle.tsx b/app/components/DocTitle.tsx similarity index 100% rename from src/components/DocTitle.tsx rename to app/components/DocTitle.tsx diff --git a/app/components/DocsCalloutBytes.tsx b/app/components/DocsCalloutBytes.tsx new file mode 100644 index 000000000..2fdeaa4a9 --- /dev/null +++ b/app/components/DocsCalloutBytes.tsx @@ -0,0 +1,18 @@ +import BytesForm from '~/components/BytesForm' + +export function DocsCalloutBytes(props: React.HTMLProps) { + return ( +
+
+
+ Subscribe to Bytes +
+

+ Your weekly dose of JavaScript news. Delivered every Monday to over + 100,000 devs, for free. +

+
+ +
+ ) +} diff --git a/app/components/DocsCalloutQueryGG.tsx b/app/components/DocsCalloutQueryGG.tsx new file mode 100644 index 000000000..818180a1c --- /dev/null +++ b/app/components/DocsCalloutQueryGG.tsx @@ -0,0 +1,53 @@ +import { LogoQueryGGSmall } from '~/components/LogoQueryGGSmall' +import CountdownTimerSmall from '~/components/CountdownTimerSmall' +import { useQueryGGPPPDiscount } from '~/hooks/useQueryGGPPPDiscount' + +export function DocsCalloutQueryGG() { + const ppp = useQueryGGPPPDiscount() + + return ( + +
+
+ Want to Skip the Docs? +
+ + + {/*
+ “If you're serious about *really* understanding React Query, there's + no better way than with query.gg” + —Tanner Linsley +
*/} + + {/*
*/} +
+

+ Launch week sale +

+

+ Up to 30% off through May 17th +

+ +
+ + {ppp && ( + <> +

+ To help make query.gg more accessible, you can enable a regional + discount of {ppp.discount * 100}% off for being in {ppp.flag}{' '} + {ppp.country}. +

+ + )} + +
+
+ ) +} diff --git a/app/components/DocsLayout.tsx b/app/components/DocsLayout.tsx new file mode 100644 index 000000000..356e705a0 --- /dev/null +++ b/app/components/DocsLayout.tsx @@ -0,0 +1,747 @@ +import * as React from 'react' +import { CgClose, CgMenuLeft } from 'react-icons/cg' +import { + FaArrowLeft, + FaArrowRight, + FaDiscord, + FaGithub, + FaTimes, +} from 'react-icons/fa' +import { + Link, + useMatches, + useNavigate, + useParams, +} from '@tanstack/react-router' +import { FrameworkSelect } from '~/components/FrameworkSelect' +import { useLocalStorage } from '~/utils/useLocalStorage' +import { DocsLogo } from '~/components/DocsLogo' +import { last, capitalize } from '~/utils/utils' +import type { SelectOption } from '~/components/FrameworkSelect' +import type { ConfigSchema, MenuItem } from '~/utils/config' +import { create } from 'zustand' +import { Framework, getFrameworkOptions } from '~/libraries' +import { DocsCalloutQueryGG } from '~/components/DocsCalloutQueryGG' +import { DocsCalloutBytes } from '~/components/DocsCalloutBytes' +import { twMerge } from 'tailwind-merge' +import { partners } from '~/utils/partners' +import { useThemeStore } from './ThemeToggle' +import { + GadFooter, + GadLeftRailSquare, + GadRightRailSquare, +} from './GoogleScripts' +import { SearchButton } from './SearchButton' + +// Let's use zustand to wrap the local storage logic. This way +// we'll get subscriptions for free and we can use it in other +// components if we need to. +const useLocalCurrentFramework = create<{ + currentFramework?: string + setCurrentFramework: (framework: string) => void +}>((set) => ({ + currentFramework: + typeof document !== 'undefined' + ? localStorage.getItem('framework') || undefined + : undefined, + setCurrentFramework: (framework: string) => { + localStorage.setItem('framework', framework) + set({ currentFramework: framework }) + }, +})) + +/** + * Use framework in URL path + * Otherwise use framework in localStorage if it exists for this project + * Otherwise fallback to react + */ +function useCurrentFramework(frameworks: Framework[]) { + const navigate = useNavigate() + + const { framework: paramsFramework } = useParams({ + strict: false, + }) + + const localCurrentFramework = useLocalCurrentFramework() + + let framework = (paramsFramework || + localCurrentFramework.currentFramework || + 'react') as Framework + + framework = frameworks.includes(framework) ? framework : 'react' + + const setFramework = React.useCallback( + (framework: string) => { + navigate({ + params: (prev) => ({ + ...prev, + framework, + }), + }) + localCurrentFramework.setCurrentFramework(framework) + }, + [localCurrentFramework, navigate] + ) + + React.useEffect(() => { + // Set the framework in localStorage if it doesn't exist + if (!localCurrentFramework.currentFramework) { + localCurrentFramework.setCurrentFramework(framework) + } + + // Set the framework in localStorage if it doesn't match the URL + if ( + paramsFramework && + paramsFramework !== localCurrentFramework.currentFramework + ) { + localCurrentFramework.setCurrentFramework(paramsFramework) + } + }) + + return { + framework, + setFramework, + } +} + +// Let's use zustand to wrap the local storage logic. This way +// we'll get subscriptions for free and we can use it in other +// components if we need to. +const useLocalCurrentVersion = create<{ + currentVersion?: string + setCurrentVersion: (version: string) => void +}>((set) => ({ + currentVersion: + typeof document !== 'undefined' + ? localStorage.getItem('version') || undefined + : undefined, + setCurrentVersion: (version: string) => { + localStorage.setItem('version', version) + set({ currentVersion: version }) + }, +})) + +/** + * Use framework in URL path + * Otherwise use framework in localStorage if it exists for this project + * Otherwise fallback to react + */ +function useCurrentVersion(versions: string[]) { + const navigate = useNavigate() + + const { version: paramsVersion } = useParams({ + strict: false, + }) + + const localCurrentVersion = useLocalCurrentVersion() + + let version = paramsVersion || localCurrentVersion.currentVersion || 'latest' + + version = versions.includes(version) ? version : 'latest' + + const setVersion = React.useCallback( + (version: string) => { + navigate({ + params: (prev: Record) => ({ + ...prev, + version, + }), + }) + localCurrentVersion.setCurrentVersion(version) + }, + [localCurrentVersion, navigate] + ) + + React.useEffect(() => { + // Set the version in localStorage if it doesn't exist + if (!localCurrentVersion.currentVersion) { + localCurrentVersion.setCurrentVersion(version) + } + + // Set the version in localStorage if it doesn't match the URL + if (paramsVersion && paramsVersion !== localCurrentVersion.currentVersion) { + localCurrentVersion.setCurrentVersion(paramsVersion) + } + }) + + return { + version, + setVersion, + } +} + +const useMenuConfig = ({ + config, + repo, + frameworks, +}: { + config: ConfigSchema + repo: string + frameworks: Framework[] +}): MenuItem[] => { + const currentFramework = useCurrentFramework(frameworks) + + const localMenu: MenuItem = { + label: '菜单', + children: [ + { + label: '首页', + to: '..', + }, + ...(frameworks.length > 1 + ? [ + { + label: '框架', + to: './framework', + }, + ] + : []), + { + label: ( +
+ GitHub +
+ ), + to: `https://github.com/${repo}`, + }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, + ], + } + + return [ + localMenu, + // Merge the two menus together based on their group labels + ...config.sections.map((section): MenuItem | undefined => { + const frameworkDocs = section.frameworks?.find( + (f) => f.label === currentFramework.framework + ) + const frameworkItems = frameworkDocs?.children ?? [] + + const children = [ + ...section.children.map((d) => ({ ...d, badge: 'core' })), + ...frameworkItems.map((d) => ({ + ...d, + badge: currentFramework.framework, + })), + ] + + if (children.length === 0) { + return undefined + } + + return { + label: section.label, + children, + collapsible: section.collapsible ?? false, + defaultCollapsed: section.defaultCollapsed ?? false, + } + }), + ].filter((item) => item !== undefined) +} + +const useFrameworkConfig = ({ frameworks }: { frameworks: Framework[] }) => { + const currentFramework = useCurrentFramework(frameworks) + + const frameworkConfig = React.useMemo(() => { + return { + label: 'Framework', + selected: frameworks.includes(currentFramework.framework) + ? currentFramework.framework + : 'react', + available: getFrameworkOptions(frameworks), + onSelect: (option: { label: string; value: string }) => { + currentFramework.setFramework(option.value) + }, + } + }, [frameworks, currentFramework]) + + return frameworkConfig +} + +const useVersionConfig = ({ versions }: { versions: string[] }) => { + const currentVersion = useCurrentVersion(versions) + + const versionConfig = React.useMemo(() => { + const available = versions.reduce( + (acc: SelectOption[], version) => { + acc.push({ + label: version, + value: version, + }) + return acc + }, + [ + { + label: 'Latest', + value: 'latest', + }, + ] + ) + + return { + label: 'Version', + selected: versions.includes(currentVersion.version) + ? currentVersion.version + : 'latest', + available, + onSelect: (option: { label: string; value: string }) => { + currentVersion.setVersion(option.value) + }, + } + }, [currentVersion, versions]) + + return versionConfig +} + +type DocsLayoutProps = { + name: string + version: string + colorFrom: string + colorTo: string + textColor: string + config: ConfigSchema + frameworks: Framework[] + versions: string[] + repo: string + children: React.ReactNode +} + +export function DocsLayout({ + name, + version, + colorFrom, + colorTo, + textColor, + config, + frameworks, + versions, + repo, + children, +}: DocsLayoutProps) { + const { libraryId } = useParams({ + from: '/$libraryId/$version/docs', + }) + const { _splat } = useParams({ strict: false }) + const frameworkConfig = useFrameworkConfig({ frameworks }) + const versionConfig = useVersionConfig({ versions }) + const menuConfig = useMenuConfig({ config, frameworks, repo }) + + const matches = useMatches() + const lastMatch = last(matches) + + const isExample = matches.some((d) => d.pathname.includes('/examples/')) + + const detailsRef = React.useRef(null!) + + const flatMenu = React.useMemo( + () => menuConfig.flatMap((d) => d?.children), + [menuConfig] + ) + + const docsMatch = matches.find((d) => d.pathname.includes('/docs')) + + const relativePathname = lastMatch.pathname.replace( + docsMatch!.pathname + '/', + '' + ) + + const index = flatMenu.findIndex((d) => d?.to === relativePathname) + const prevItem = flatMenu[index - 1] + const nextItem = flatMenu[index + 1] + + const [showBytes, setShowBytes] = useLocalStorage('showBytes', true) + + const [mounted, setMounted] = React.useState(false) + + React.useEffect(() => { + setMounted(true) + }, []) + + const menuItems = menuConfig.map((group, i) => { + const WrapperComp = group.collapsible ? 'details' : 'div' + const LabelComp = group.collapsible ? 'summary' : 'div' + + const isChildActive = group.children.some((d) => d.to === _splat) + const configGroupOpenState = + typeof group.defaultCollapsed !== 'undefined' + ? !group.defaultCollapsed // defaultCollapsed is true means the group is closed + : undefined + const isOpen = isChildActive ? true : configGroupOpenState ?? false + + const detailsProps = group.collapsible ? { open: isOpen } : {} + + return ( + + + {group?.label} + +
+
    + {group?.children?.map((child, i) => { + const linkClasses = `cursor-pointer flex gap-2 items-center justify-between group px-2 py-[.1rem] rounded-lg hover:bg-gray-500 hover:bg-opacity-10` + + return ( +
  • + {child.to.startsWith('http') ? ( + + {child.label} + + ) : ( + { + detailsRef.current.removeAttribute('open') + }} + activeOptions={{ + exact: true, + includeHash: false, + includeSearch: false, + }} + className="!cursor-pointer relative" + > + {(props) => { + return ( +
    +
    + {/*
    */} + {child.label} + {/*
    */} +
    + {child.badge ? ( +
    + {child.badge} +
    + ) : null} +
    + ) + }} + + )} +
  • + ) + })} +
+ + ) + }) + + const logo = ( + + ) + + const smallMenu = ( +
+
+ +
+ + + {logo} +
+
+
+
+ + +
+ + {menuItems} +
+
+
+ ) + + const largeMenu = ( +
+
+ {logo} +
+
+ +
+
+ + +
+
+ {menuItems} +
+
+ ) + + return ( +
+ {smallMenu} + {largeMenu} +
+
+ {children} +
+
+ +
+
+
+ {prevItem ? ( + +
+ + {prevItem.label} +
+ + ) : null} +
+
+ {nextItem ? ( + +
+ + {nextItem.label} + {' '} + +
+ + ) : null} +
+
+
+
+
+
+
+ Our Partners +
+ {!partners.some((d) => d.libraries?.includes(libraryId as any)) ? ( + + ) : ( + partners + .filter((d) => d.sidebarImgLight) + .filter((d) => d.libraries?.includes(libraryId as any)) + .map((partner) => { + return ( +
+ +
+ {partner.name} + {partner.name} +
+
+ {partner.sidebarAfterImg || null} +
+ ) + }) + )} +
+ {libraryId === 'query' ? ( +
+ +
+ ) : null} + +
+ +
+ +
+ +
+ + {/*
+ +
*/} + + {libraryId !== 'query' ? ( +
+ +
+ ) : null} +
+
+ {showBytes ? ( +
+
+ {libraryId === 'query' ? ( + + ) : ( + + )} + +
+
+ ) : ( + + )} +
+ ) +} diff --git a/app/components/DocsLogo.tsx b/app/components/DocsLogo.tsx new file mode 100644 index 000000000..6ebb60839 --- /dev/null +++ b/app/components/DocsLogo.tsx @@ -0,0 +1,41 @@ +import { Link, useLocation } from '@tanstack/react-router' +import { ThemeToggle } from './ThemeToggle' +import { I18nToggle } from '@tanstack-dev/components' + +type Props = { + name: string + libraryId: string + version: string + colorFrom: string + colorTo: string +} + +export const DocsLogo = ({ + name, + version, + colorFrom, + colorTo, + libraryId, +}: Props) => { + const gradientText = `inline-block text-transparent bg-clip-text bg-gradient-to-r ${colorFrom} ${colorTo}` + const href = useLocation().href; + return ( + <> + + TanStack + + + {name}{' '} + {version} + + +
+ +
+ + ) +} diff --git a/src/components/FileExplorer.tsx b/app/components/FileExplorer.tsx similarity index 80% rename from src/components/FileExplorer.tsx rename to app/components/FileExplorer.tsx index 13d36fc85..dbb85812e 100644 --- a/src/components/FileExplorer.tsx +++ b/app/components/FileExplorer.tsx @@ -6,11 +6,8 @@ import htmlIconUrl from '~/images/file-icons/html.svg?url' import jsonIconUrl from '~/images/file-icons/json.svg?url' import svelteIconUrl from '~/images/file-icons/svelte.svg?url' import vueIconUrl from '~/images/file-icons/vue.svg?url' -import markoIconUrl from '~/images/file-icons/marko.svg?url' -import emberIconUrl from '~/images/ember-logo.svg?url' import textIconUrl from '~/images/file-icons/txt.svg?url' import type { GitHubFileNode } from '~/utils/documents.server' -import { twMerge } from 'tailwind-merge' const getFileIconPath = (filename: string) => { const ext = filename.split('.').pop()?.toLowerCase() || '' @@ -32,11 +29,6 @@ const getFileIconPath = (filename: string) => { return svelteIconUrl case 'vue': return vueIconUrl - case 'marko': - return markoIconUrl - case 'gjs': - case 'gts': - return emberIconUrl default: return textIconUrl } @@ -142,7 +134,7 @@ export function FileExplorer({ } } return expanded - }, + } ) const startResizeRef = React.useRef({ @@ -218,7 +210,7 @@ export function FileExplorer({ width: isSidebarOpen ? sidebarWidth : 0, paddingRight: isSidebarOpen ? '0.5rem' : 0, }} - className={`shrink-0 overflow-y-auto bg-linear-to-r from-gray-50 via-gray-50 to-transparent dark:from-gray-800/50 dark:via-gray-800/50 dark:to-transparent shadow-sm ${ + className={`flex-shrink-0 overflow-y-auto bg-gradient-to-r from-gray-50 via-gray-50 to-transparent dark:from-gray-800/50 dark:via-gray-800/50 dark:to-transparent shadow-sm ${ isResizing ? '' : 'transition-all duration-300' }`} > @@ -236,7 +228,6 @@ export function FileExplorer({
) : null}
- {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
void }) => { - const pendingPrefetchesRef = React.useRef>({}) - - const cancelPrefetch = (path: string) => { - const timeoutId = pendingPrefetchesRef.current[path] - - if (timeoutId !== undefined) { - window.clearTimeout(timeoutId) - delete pendingPrefetchesRef.current[path] - } - } - - const schedulePrefetch = (path: string) => { - cancelPrefetch(path) - - pendingPrefetchesRef.current[path] = window.setTimeout(() => { - delete pendingPrefetchesRef.current[path] - props.prefetchFileContent(path) - }, 180) - } - - React.useEffect(() => { - const pendingPrefetches = pendingPrefetchesRef.current - - return () => { - for (const timeoutId of Object.values(pendingPrefetches)) { - window.clearTimeout(timeoutId) - } - } - }, []) - if (!props.files) return null return (