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-hant/ag-grid-partnership.md b/app/blog/zh-hant/ag-grid-partnership.md new file mode 100644 index 000000000..20bbaa480 --- /dev/null +++ b/app/blog/zh-hant/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-hant/announcing-tanstack-form-v1.md b/app/blog/zh-hant/announcing-tanstack-form-v1.md new file mode 100644 index 000000000..bd363315c --- /dev/null +++ b/app/blog/zh-hant/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 +``` + +# 一段歷史 + +差不多兩年前,[我看到 Tanner 在 BlueSky(當時是僅邀請平台)上發佈了一則消息,宣布他正在開發一個新項目: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) + +當時,我剛剛為 React 推出了一個名為 "[HouseForm](https://web.archive.org/web/20240101000000*/houseform.dev)" 的替代表單庫,我立即被 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)。一切都從您的運行時使用中推斷出來。 + +## 模式驗證 + +感謝 [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-hant/announcing-tanstack-query-v4.md b/app/blog/zh-hant/announcing-tanstack-query-v4.md new file mode 100644 index 000000000..2405f8d60 --- /dev/null +++ b/app/blog/zh-hant/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` 🎉。 +重新品牌命名和重組為單一代碼庫現在終於使我們能夠將 `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 以來,持久器存在於實驗功能中。它們允許您將查詢緩存同步到外部位置(例如 localStorage)以供日後使用。在得到大量反饋後,我們重新設計並改進了 API,現在我們提供兩個主要的持久器: + +- SyncStoragePersister +- AsyncStoragePersister + +這些持久器對於大多數用例都能很好地工作,但沒有什麼能阻止你編寫自己的持久器 - 只要它符合所需的接口即可。 + +### 支持 React 18 + +React 18 在今年早些時候發布,v4 現在完全支持它及其帶來的新並發功能。為了實現這一點,內部訂閱被重寫以利用新的 `useSyncExternalStore` 鉤子。 + +### 默認啟用追蹤查詢 + +追蹤查詢是一種性能優化,在 v3.6.0 中作為可選功能添加。這種優化現在是 v4 中的默認行為,這應該為您提供良好的渲染性能提升。 + +### 精簡的 API + +隨著時間的推移,我們的一些 API 變得相當複雜,以至於它們相互矛盾。一些例子包括: + +- QueryKeys 有時暴露時被轉換為數組,有時不是。 +- Query Filters 不直觀且相互排斥。 +- 不同方法上參數的默認值默認為相反的值 + +我們清理了許多這些不一致之處,以使開發者體驗更好。v4 還帶有一個代碼轉換工具,幫助您完成遷移路徑。 + +## 接下來是什麼? + +首先是清理文檔。你可能已經注意到,它們仍然相當特定於 react,並且不時地引用 `react-query`。請耐心等待,因為我們旨在重新構建文檔,PRs 永遠受歡迎。 + +同時,還有更多適配器。目前,只有 React 適配器存在,但我們渴望很快添加更多框架。 diff --git a/app/blog/zh-hant/announcing-tanstack-query-v5.md b/app/blog/zh-hant/announcing-tanstack-query-v5.md new file mode 100644 index 000000000..6fcdb734d --- /dev/null +++ b/app/blog/zh-hant/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`,並 [移除了回調](https://github.com/TanStack/query/discussions/5279) 從 `useQuery`。所有這些變化使 v5 成為新手入門的最一致和最佳版本。 + +要了解更多關於突破性變化的信息,請查看我們的[遷移指南](/query/v5/docs/react/guides/migrating-to-v5)。 + +## 新功能 + +當然,v5 也帶來了許多令人驚嘆的新功能 🚀: + +### 簡化的樂觀更新 + +通過利用 `useMutation` 返回的 `variables`,享受全新的、簡化的執行樂觀更新的方式,無需手動編寫更新緩存的代碼。有關更多詳情,請查看[樂觀更新文檔](/query/v5/docs/react/guides/optimistic-updates) + +### 可共享的變異狀態 + +一個經常被請求的功能,如這個 [兩年前的問題](https://github.com/TanStack/query/issues/2304) 所見,終於在 v5 中實現:現在你可以通過新的 [useMutationState](/query/v5/docs/react/reference/useMutationState) 鉤子訪問所有變異的狀態,在組件之間共享。 + +### 一流的 `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-hant/netlify-partnership.md b/app/blog/zh-hant/netlify-partnership.md new file mode 100644 index 000000000..819d7da73 --- /dev/null +++ b/app/blog/zh-hant/netlify-partnership.md @@ -0,0 +1,46 @@ +--- +title: TanStack + Netlify 合作關係 +published: 2025-03-18 +authors: + - Tanner Linsley +--- + +![Netlify Header](/blog-assets/netlify-partnership/header.jpg) + +我們很興奮地宣布,**Netlify** 現在是 **TanStack Start** 的**官方部署合作夥伴**!我們將共同努力,讓開發者更輕鬆地構建和部署現代、類型安全、以用戶為中心的網絡應用程序。 + +Netlify 作為現代網頁開發者的終極部署平台贏得了其聲譽。它對速度、簡潔、模塊化和靈活性的關注與 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 如何協同簡化現代網絡開發的絕佳方式。 + +## 接下來是什麼? + +我們才剛剛開始。期待更多更新、新功能以及 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-hant/tanstack-router-typescript-performance.md b/app/blog/zh-hant/tanstack-router-typescript-performance.md new file mode 100644 index 000000000..dd0b64d14 --- /dev/null +++ b/app/blog/zh-hant/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 個路由定義,所有這些定義都使用 `zod` 進行 `validateSearch` 和通過路由的 `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` 等進行更改,推斷的類型總是立即反映出來。 + +DX 沒有改變,但編輯器中的性能感覺非常棒(特別是當您處理大型路由樹時)。 + +## 基本規則 + +這一變化涉及對 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-hant/why-tanstack-start-and-router.md b/app/blog/zh-hant/why-tanstack-start-and-router.md new file mode 100644 index 000000000..921e53615 --- /dev/null +++ b/app/blog/zh-hant/why-tanstack-start-and-router.md @@ -0,0 +1,74 @@ +--- +title: 為什麼選擇 TanStack Start 和 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) + +構建現代網絡應用程序絕非易事。我們選擇的框架和工具不僅會影響開發體驗,還會決定我們所構建應用程序的成敗。雖然市場上有許多優秀的框架,但我相信 **TanStack Router** 和 **TanStack Start** 因其解決當今開發人員面臨挑戰的能力以及對未來的準備而脫穎而出。 + +這些不僅僅是另一套工具 —— 它們代表著以更少的摩擦、更多的樂趣來構建更好應用程序的承諾。以下是為什麼我認為您會像我一樣喜歡使用它們的原因。 + +### 可靠的類型安全 + +類型安全不僅僅是一個流行詞 —— 它是創建穩健、可維護應用程序的基礎工具。TanStack Router 提供了超越基礎的**上下文類型安全**,使類型能夠在應用程序的每個部分之間無縫流動。路由定義、參數、導航,甚至狀態管理都與完全推斷的類型協同工作。 + +這對您意味著什麼?這意味著不必再猜測您是否正確定義了參數,不必再調試類型不匹配的問題,也無需使用額外的插件或 AST 轉換來填補空白。TanStack Router 與 TypeScript 的自然架構配合得天衣無縫,使體驗順暢、可預測且令人愉悅。 + +這種級別的類型安全不僅可以節省時間 —— 還能建立信心。我相信其他框架將花費數年時間才能趕上這一點。 + +### 釋放 URL 狀態管理的力量 + +在網頁開發中,最被忽視但最強大的工具之一就是 URL。它是原始的狀態管理系統 —— 快速、可共享且直觀。然而,許多框架將 URL 視為次要考慮因素,僅提供基本工具來讀寫狀態。 + +TanStack Router 徹底改變了這一點。在 URL 中管理狀態不僅被支持 —— 還被鼓勵。通過直觀的 API,您可以使用內置的類型安全和運行時驗證來驗證、讀取和更新搜索參數。想要創建一個深度嵌套的動態過濾系統或將應用程序的狀態與 URL 同步?易如反掌。 + +但這不僅僅是關於開發人員的便利性 —— 還關乎創造更好的用戶體驗。當您的應用程序狀態存在於 URL 中時,用戶可以共享、收藏,並從他們離開的位置繼續操作。TanStack Router 讓這一切變得輕而易舉。 + +### 熟悉的模式,更多的靈活性 + +如果您曾經使用過 Remix 或 Next.js,您會在 TanStack Start 中發現許多熟悉的概念。但熟悉並不意味著妥協。我們採用了這些框架中的一些最佳理念並將其發揚光大,消除約束,提供更多靈活性。 + +例如,如果您來自於像 Remix 這樣的服務器優先框架,您會發現路由模式和服務器函數集成感覺很自然,但它們也同樣適用於傳統的客戶端單頁應用程序。您不必選邊站隊 —— 您可以兼顧兩種優勢,且幾乎沒有妥協。 + +### 為未來而建(並已經擁抱未來) + +網絡發展迅速。隨著 React Server Components (RSCs) 即將到來,React 19 引入新模式,以及流式傳輸成為數據傳輸的標準,框架需要做的不僅僅是跟上步伐 —— 它們需要引領潮流。 + +TanStack Start 已為未來做好準備。RSC 被視為另一種服務器端狀態,具有強大的原語用於緩存、失效和將它們組合到您的應用程序中。流式傳輸不是事後才想到的 —— 它已內置於 TanStack 的核心工作方式中,為您提供工具,無需額外複雜性即可增量地將數據和 HTML 發送到客戶端。 + +但我們不僅僅是著眼於未來。TanStack Start 同時也讓這些先進功能變得平易近人,並可以立即使用。您不需要等待"下一個重大事件"就可以開始構建感覺像未來的應用程序。 + +### 單頁應用並未死亡(我們只是讓它們變得更好) + +現在有很多關於服務器優先架構的討論,雖然它們令人興奮,但這並不是全部。單頁應用程序 (SPAs) 仍然是構建快速、互動性強的應用程序的絕佳方式 —— 特別是當做得正確時。 + +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.com/router) 和 [TanStack Start](https://tanstack.com/start),看看應用程序開發可以有多好。 + +讓我們一起打造令人驚嘆的產品! diff --git a/app/blog/zh-hant/why-tanstack-start-is-ditching-adapters.md b/app/blog/zh-hant/why-tanstack-start-is-ditching-adapters.md new file mode 100644 index 000000000..654ddb674 --- /dev/null +++ b/app/blog/zh-hant/why-tanstack-start-is-ditching-adapters.md @@ -0,0 +1,88 @@ +--- +title: 為什麼 TanStack Start 放棄適配器 +published: 2024-11-22 +authors: + - Tanner Linsley +--- + +![Nitro Header](/blog-assets/why-tanstack-start-is-ditching-adapters/nitro.jpg) + +## 要不要使用「適配器」? + +正如我在構建 TanStack Start(我的新型 TanStack 驅動的全棧框架)過程中所學到的,構建一個新的前端 Javascript 框架是一項艱巨的任務。這裡有許多活動部件: + +- 路由 +- 服務器端渲染 +- RPC 和 API +- 開發工具 +- **部署與託管** + +最後一項,**部署與託管**尤其棘手,因為現在似乎每個雲環境都有自己獨特的方式來使事情「正常運行」。當面對這個 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 與眾不同的內容: + +- 一流的完全類型安全路由器 +- 用於構建服務器相關 RPC 的靈活原語 +- 可選的服務器功能(SSR、API、RSC 等) +- 與其他 TanStack 原語(如 TanStack Query)的深度集成 +- 以及更多即將推出的功能! + +## 更進一步 + +能夠將這麼多工作卸載給 Nitro 和 Vite 並獲得如此多的出色功能真是太棒了,但這絕對不是使用託管平台(尤其是 Vercel)*每個*功能的 100% 完整解決方案,在 Vercel 中我們可以訪問的不僅僅是部署。我們還在考慮更多功能,例如[邊緣網絡緩存](https://vercel.com/docs/edge-network/caching)以及我個人最喜歡的[\*偏移保護](https://vercel.com/docs/deployments/skew-protection)\*。 + +例如,偏移保護(確保客戶端和服務器對各自的部署保持同步)需要的不僅僅是構建步驟。它還涉及能夠在運行時將平台原語深度集成到框架中,或者在特定情況下,能夠將特定的 cookie 或標頭注入到針對 Vercel 的傳出 API/服務器請求中。 + +我很高興地報告,TanStack Start 將附帶令人驚嘆的強大中間件原語(適用於 API 路由和服務器函數 RPC),這將使其成為一行程序,甚至可能是自動的(希望如此)。 + +這種級別的開發者體驗和集成正是讓我對未來感到興奮的原因,我相信這也是開源的真正意義所在:將生態系統中強大的工具組合在一起,為開發者和用戶提供驚人的體驗。 + +我想不出比 TanStack Start、Nitro、Vite 和 Vercel 更好的組合,為您提供一流的網絡應用體驗。 + +## 60 秒內嘗試 + +TanStack Start 目前處於 Beta 階段!點擊下面的["部署"](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 分鐘內在 Vercel 上創建和部署 TanStack Start "Basic" 模板的新副本。 + +[![使用 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..86ae3b58d --- /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 && ( +
+ + We use cookies for site functionality, analytics, and ads{' '} + + (which is a large part of how TanStack OSS remains free forever) + + . See our{' '} + + Privacy Policy + {' '} + for details. + +
+ + + +
+
+ )} + + {showSettings && ( +
+
+

Cookie Settings

+
+ + +
+ +
+
+
+
+ )} + + ) +} 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..622d6b7d5 --- /dev/null +++ b/app/components/DocsLayout.tsx @@ -0,0 +1,748 @@ +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: 'Menu', + children: [ + { + label: 'Home', + to: '..', + }, + ...(frameworks.length > 1 + ? [ + { + label: 'Frameworks', + 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..d011f4a6a --- /dev/null +++ b/app/components/DocsLogo.tsx @@ -0,0 +1,39 @@ +import { Link } from '@tanstack/react-router' +import { ThemeToggle } from './ThemeToggle' + +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}` + + 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 (