From 3581aec569125a28d7024fec8cdc8af152b4cbd2 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 20 Feb 2023 09:41:43 -0700 Subject: [PATCH] checkpoint --- docs/api/react/createRouteConfig.md | 10 +- docs/api/react/useLoaderData.md | 6 +- docs/config.json | 26 +- docs/guide/code-splitting.md | 6 +- docs/guide/data-loading.md | 64 +- docs/guide/data-mutations.md | 4 +- docs/guide/preloading.md | 4 +- docs/guide/route-paths.md | 2 +- docs/guide/router-context.md | 8 +- docs/guide/search-params.md | 4 +- examples/react/basic/src/main.tsx | 63 +- .../src/routes/dashboard/dashboard.tsx | 4 +- .../src/routes/dashboard/index.tsx | 2 +- .../src/routes/dashboard/invoices/index.tsx | 4 +- .../src/routes/dashboard/invoices/invoice.tsx | 6 +- .../src/routes/dashboard/users/index.tsx | 6 +- .../src/routes/dashboard/users/user.tsx | 6 +- .../src/routes/layout/index.tsx | 6 +- .../chunks/Hydrate.f13f5eaa.mjs | 3976 +++++++++-------- .../chunks/Hydrate.ff008fd4.mjs | 3976 +++++++++-------- .../chunks/routeTree.2e12cfef.mjs | 18 +- .../chunks/routeTree.c870053d.mjs | 18 +- .../chunks/routeTree.f436718c.mjs | 18 +- .../with-astro-ssr/src/app/routes/posts.tsx | 6 +- .../src/app/routes/posts/$postId.tsx | 6 +- examples/react/with-react-query/src/main.tsx | 4 +- .../with-trpc-react-query/client/main.tsx | 4 +- examples/react/with-trpc/client/main.tsx | 12 +- packages/loaders/src/index.ts | 33 +- packages/react-loaders/src/index.tsx | 16 +- .../router/__tests__/createRoutes.test.ts | 8 +- packages/router/__tests__/index.test.tsx | 16 +- packages/router/src/react.tsx | 17 + packages/router/src/route.ts | 158 +- packages/router/src/routeMatch.ts | 6 +- 35 files changed, 4709 insertions(+), 3814 deletions(-) diff --git a/docs/api/react/createRouteConfig.md b/docs/api/react/createRouteConfig.md index 4f3bed5ae3..8f948be732 100644 --- a/docs/api/react/createRouteConfig.md +++ b/docs/api/react/createRouteConfig.md @@ -67,13 +67,13 @@ const router = new Route() }) ``` - - `onLoad: LoaderFn` + - `loader: LoaderFn` - This is used when you want you load data in your route which can later be read using 'useLoaderInstance' or 'useMatch' + This is used when you want you load data in your route which can later be read using 'useLoader' or 'useMatch' ```tsx const rootRouter = route({ - onLoad: async () => { + loader: async () => { const posts = await axios .get('https://jsonplaceholder.typicode.com/posts') .then((r) => r.data.slice(0, 10)) @@ -110,14 +110,14 @@ search: {}}) => void | ((match: {params: {}; search: {}; }) => void) | undefined) | undefined ` - This function is called when moving from an inactive state to an active one. Likewise, when moving from an active to an inactive state, the return function (if provided) is called. + This function is called when moving from an inactive state to an active one. Likewise, when moving from an active to an inactive state, the return function (if provided) is called. - `onTransition: ((match: { params: {}; search: {}; }) => void) | undefined ` - This function is called when the route remains active from one transition to the next. + This function is called when the route remains active from one transition to the next. - `parseParams: ((rawParams: Record) => Record) | undefined` diff --git a/docs/api/react/useLoaderData.md b/docs/api/react/useLoaderData.md index 6ebcb67f2f..81b2ba5257 100644 --- a/docs/api/react/useLoaderData.md +++ b/docs/api/react/useLoaderData.md @@ -1,10 +1,10 @@ --- -id: useLoaderInstance -title: useLoaderInstance +id: useLoader +title: useLoader --- ```tsx -const data = useLoaderInstance({ +const data = useLoader({ from, strict, select, diff --git a/docs/config.json b/docs/config.json index 08a0a6fc9e..24a12d3105 100644 --- a/docs/config.json +++ b/docs/config.json @@ -183,8 +183,8 @@ "to": "api/react/useLinkProps" }, { - "label": "useLoaderInstance", - "to": "api/react/useLoaderInstance" + "label": "useLoader", + "to": "api/react/useLoader" }, { "label": "useMatch", @@ -241,29 +241,9 @@ "to": "examples/react/basic?file=src%2Fmain.tsx" }, { - "label": "Basic SSR", - "to": "examples/react/basic-ssr?file=src%2Froutes%2F__root.tsx" - }, - { - "label": "Basic SSR w/ ", - "to": "examples/react/basic-ssr-with-html?file=src%2Froutes%2F__root.tsx" - }, - { - "label": "Basic SSR Streaming (WIP)", - "to": "examples/react/basic-ssr-streaming?file=src%2Froutes%2F__root.tsx" - }, - { - "label": "Kitchen Sink Single File", - "to": "examples/react/kitchen-sink-single-file?file=src%2Fmain.tsx" - }, - { - "label": "Kitchen Sink Multi-File", + "label": "Kitchen Sink", "to": "examples/react/kitchen-sink-multi-file?file=src%2Fmain.tsx" }, - { - "label": "Kitchen Sink SSR", - "to": "examples/react/kitchen-sink-ssr?file=src%2Froutes%2F__root.tsx" - }, { "label": "Astro SSR", "to": "examples/react/with-astro-ssr?file=src%2Froutes%2F__root.tsx" diff --git a/docs/guide/code-splitting.md b/docs/guide/code-splitting.md index bd717a7b6d..2171cdb0e0 100644 --- a/docs/guide/code-splitting.md +++ b/docs/guide/code-splitting.md @@ -20,7 +20,7 @@ const route = new Route({ ### Using the TanStack Router framework adapters `lazy` wrapper -Regardless of framework, if a route component has a static `preload` method attached to it, the Router will preload it when the route is matched with the `onLoad` option. +Regardless of framework, if a route component has a static `preload` method attached to it, the Router will preload it when the route is matched with the `loader` option. If we were to **set this up manually** (so... don't do this) in React, it would look something like this: @@ -50,13 +50,13 @@ The `lazy` wrapper not only implements `React.lazy()`, but automatically sets up ## Data Loader Splitting -Regardless of which data loading library you decide to go with, you may end up with a lot of data loaders that could potentially contribute to a large bundle size. If this is the case, you can code split your data loading logic using the Route's `onLoad` option: +Regardless of which data loading library you decide to go with, you may end up with a lot of data loaders that could potentially contribute to a large bundle size. If this is the case, you can code split your data loading logic using the Route's `loader` option: ```tsx const route = new Route({ path: '/my-route', component: MyComponent, - onLoad: async () => { + loader: async () => { const data = await import('./data') return { data } }, diff --git a/docs/guide/data-loading.md b/docs/guide/data-loading.md index faa5c1cd96..7cea4b38d1 100644 --- a/docs/guide/data-loading.md +++ b/docs/guide/data-loading.md @@ -43,9 +43,9 @@ Just because TanStack Router works with any data-fetching library doesn't mean w For the following examples, we'll show you the basics of data loading using **TanStack Loaders**, but as we've already mentioned, these same principles can be applied to any state management library worth it's salt. Let's get started! -## The `onLoad` route option +## The `loader` route option -The `onLoad` route option is a function that is called **every time** a route is matched and loaded for: +The `loader` route option is a function that is called **every time** a route is matched and loaded for: - Navigating to a new route - Refreshing the current route @@ -53,15 +53,15 @@ The `onLoad` route option is a function that is called **every time** a route is Let's repeat that again. **Every time** someone navigates to a new route, refreshes the current route, or preloads a route, the matching routes' `onload` functions will be called. -> ⚠️ If you've used Remix or Next.js, you may be used to the idea that data loading only happens for routes on the page that _change_ when navigating. eg. If you were to navigate from `/posts` to `/posts/1`, the `loader`/`getServerSideProps `function for `/posts` would not be called again. This is not the case with TanStack Router. Every route's `onLoad` function will be called every time a route is loaded. +> ⚠️ If you've used Remix or Next.js, you may be used to the idea that data loading only happens for routes on the page that _change_ when navigating. eg. If you were to navigate from `/posts` to `/posts/1`, the `loader`/`getServerSideProps `function for `/posts` would not be called again. This is not the case with TanStack Router. Every route's `loader` function will be called every time a route is loaded. -The biggest reason for calling `onLoad` every time is to notify your data loading library that data is or will be required. How your data loading library uses that information may vary, but obviously, this pattern is hopeful that your data fetching library can cache and refetch in the background. If you're using TanStack Loaders or TanStack Query, this is the default behavior. +The biggest reason for calling `loader` every time is to notify your data loading library that data is or will be required. How your data loading library uses that information may vary, but obviously, this pattern is hopeful that your data fetching library can cache and refetch in the background. If you're using TanStack Loaders or TanStack Query, this is the default behavior. -Here is a simple example of using `onLoad` to fetch data for a route: +Here is a simple example of using `loader` to fetch data for a route: ```tsx import { Route } from '@tanstack/router' -import { Loader, useLoaderInstance } from '@tanstack/react-loaders' +import { Loader, useLoader } from '@tanstack/react-loaders' const postsLoader = new Loader({ key: 'posts', @@ -75,22 +75,22 @@ const postsLoader = new Loader({ const postsRoute = new Route({ getParentPath: () => rootRoute, path: 'posts', - async onLoad() { + async loader() { // Wait for the loader to finish await postsLoader.load() }, component: () => { - // Access the loader's data, in this case with the useLoaderInstance hook - const posts = useLoaderInstance({ loader: postsLoader }) + // Access the loader's data, in this case with the useLoader hook + const posts = useLoader({ loader: postsLoader }) return
...
}, }) ``` -## `onLoad` Parameters +## `loader` Parameters -The `onLoad` function receives a single parameter, which is an object with the following properties: +The `loader` function receives a single parameter, which is an object with the following properties: - `params` - The route's parsed path params - `search` - The route's search query, parsed, validated and typed **including** inherited search params from parent routes @@ -98,17 +98,17 @@ The `onLoad` function receives a single parameter, which is an object with the f - `hash` - The route's hash - `context` - The route's context object **including** inherited context from parent routes - `routeContext` - The route's context object, **excluding** inherited context from parent routes -- `signal` - The route's abort signal which is cancelled when the route is unloaded or when the `onLoad` call becomes outdated. +- `signal` - The route's abort signal which is cancelled when the route is unloaded or when the `loader` call becomes outdated. Using these parameters, we can do a lot of cool things. Let's take a look at a few examples ## Using Path Params -The `params` property of the `onLoad` function is an object containing the route's path params. +The `params` property of the `loader` function is an object containing the route's path params. ```tsx import { Route } from '@tanstack/router' -import { Loader, useLoaderInstance } from '@tanstack/react-loaders' +import { Loader, useLoader } from '@tanstack/react-loaders' const postLoader = new Loader({ key: 'post', @@ -123,12 +123,12 @@ const postLoader = new Loader({ const postRoute = new Route({ getParentPath: () => postsRoute, path: '$postId', - async onLoad({ params }) { + async loader({ params }) { await postLoader.load({ variables: params.postId }) }, component: () => { const { postId } = useParams({ from: postRoute.id }) - const posts = useLoaderInstance({ loader: postLoader, variables: postId }) + const posts = useLoader({ loader: postLoader, variables: postId }) return
...
}, @@ -137,11 +137,11 @@ const postRoute = new Route({ ## Using Search Params -The `search` and `routeSearch` properties of the `onLoad` function are objects containing the route's search params. `search` contains _all_ of the search params including parent search params. `routeSearch` only includes specific search params from this route. In this example, we'll use zod to validate and parse the search params for `/posts/$postId` route and use them in an `onload` function and our component. +The `search` and `routeSearch` properties of the `loader` function are objects containing the route's search params. `search` contains _all_ of the search params including parent search params. `routeSearch` only includes specific search params from this route. In this example, we'll use zod to validate and parse the search params for `/posts/$postId` route and use them in an `onload` function and our component. ```tsx import { Route } from '@tanstack/router' -import { Loader, useLoaderInstance } from '@tanstack/react-loaders' +import { Loader, useLoader } from '@tanstack/react-loaders' const postsLoader = new Loader({ key: 'posts', @@ -159,12 +159,12 @@ const postsRoute = new Route({ validateSearch: z.object({ pageIndex: z.number().int().nonnegative().catch(0), }), - async onLoad({ search }) { + async loader({ search }) { await postsLoader.load({ variables: search.pageIndex }) }, component: () => { const search = useSearchParams({ from: postsRoute.id }) - const posts = useLoaderInstance({ + const posts = useLoader({ loader: postsLoader, variables: search.pageIndex, }) @@ -176,13 +176,13 @@ const postsRoute = new Route({ ## Using Context -The `context` and `routeContext` properties of the `onLoad` function are objects containing the route's context. `context` is the context object for the route including context from parent routes. `routeContext` is the context object for the route excluding context from parent routes. In this example, we'll create a `loaderClient` and inject it into our router's context. We'll then use that client in our `onLoad` function and our component. +The `context` and `routeContext` properties of the `loader` function are objects containing the route's context. `context` is the context object for the route including context from parent routes. `routeContext` is the context object for the route excluding context from parent routes. In this example, we'll create a `loaderClient` and inject it into our router's context. We'll then use that client in our `loader` function and our component. > 🧠 Context is a powerful tool for dependency injection. You can use it to inject services, loaders, and other objects into your router and routes. You can also additively pass data down the route tree at every route using a route's `getContext` option. ```tsx import { Route } from '@tanstack/router' -import { Loader, useLoaderInstance } from '@tanstack/react-loaders' +import { Loader, useLoader } from '@tanstack/react-loaders' const postsLoader = new Loader({ key: 'posts', @@ -211,11 +211,11 @@ const rootRoute = RootRoute.withRouterContext<{ const postsRoute = new Route({ getParentPath: () => rootRoute, path: 'posts', - async onLoad({ context }) { + async loader({ context }) { await context.loaderClient.getLoader({ key: 'posts' }).load() }, component: () => { - const posts = useLoaderInstance({ key: 'posts' }) + const posts = useLoader({ key: 'posts' }) return
...
}, @@ -234,11 +234,11 @@ const router = new Router({ ## Using the Abort Signal -The `signal` property of the `onLoad` function is an [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) which is cancelled when the route is unloaded or when the `onLoad` call becomes outdated. This is useful for cancelling network requests when the route is unloaded or when the route's params change. Here is an example using TanStack Loader's signal passthrough: +The `signal` property of the `loader` function is an [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) which is cancelled when the route is unloaded or when the `loader` call becomes outdated. This is useful for cancelling network requests when the route is unloaded or when the route's params change. Here is an example using TanStack Loader's signal passthrough: ```tsx import { Route } from '@tanstack/router' -import { Loader, useLoaderInstance } from '@tanstack/react-loaders' +import { Loader, useLoader } from '@tanstack/react-loaders' const postsLoader = new Loader({ key: 'posts', @@ -253,12 +253,12 @@ const postsLoader = new Loader({ const postsRoute = new Route({ getParentPath: () => rootRoute, path: 'posts', - async onLoad({ signal }) { + async loader({ signal }) { // Pass the route's signal to the loader await postsLoader.load({ signal }) }, component: () => { - const posts = useLoaderInstance({ loader: postsLoader }) + const posts = useLoader({ loader: postsLoader }) return
...
}, @@ -267,11 +267,11 @@ const postsRoute = new Route({ ## Using the `prefetch` flag -The `prefetch` property of the `onLoad` function is a boolean which is `true` when the route is being loaded via a prefetch action. Some data loading libraries may handle prefetching differently than a standard fetch, so you may want to pass `prefetch` to your data loading library, or use it to execute the appropriate data loading logic. Here is an example using TanStack Loader and it's built-in `prefetch` flag: +The `prefetch` property of the `loader` function is a boolean which is `true` when the route is being loaded via a prefetch action. Some data loading libraries may handle prefetching differently than a standard fetch, so you may want to pass `prefetch` to your data loading library, or use it to execute the appropriate data loading logic. Here is an example using TanStack Loader and it's built-in `prefetch` flag: ```tsx import { Route } from '@tanstack/router' -import { Loader, useLoaderInstance } from '@tanstack/react-loaders' +import { Loader, useLoader } from '@tanstack/react-loaders' const postsLoader = new Loader({ key: 'posts', @@ -285,12 +285,12 @@ const postsLoader = new Loader({ const postsRoute = new Route({ getParentPath: () => rootRoute, path: 'posts', - async onLoad({ prefetch }) { + async loader({ prefetch }) { // Pass the route's prefetch to the loader await postsLoader.load({ prefetch }) }, component: () => { - const posts = useLoaderInstance({ loader: postsLoader }) + const posts = useLoader({ loader: postsLoader }) return
...
}, diff --git a/docs/guide/data-mutations.md b/docs/guide/data-mutations.md index 58aad90004..d758df683a 100644 --- a/docs/guide/data-mutations.md +++ b/docs/guide/data-mutations.md @@ -68,7 +68,7 @@ import { useAction } from '@tanstack/react-actions' function PostEditor() { const params = useParams({ from: postEditRoute.id }) - const postLoader = useLoaderInstance({ + const postLoader = useLoader({ key: 'post', variables: params.postId, }) @@ -158,7 +158,7 @@ import { useAction } from '@tanstack/react-actions' function PostEditor() { const params = useParams({ from: postEditRoute.id }) - const postLoader = useLoaderInstance({ + const postLoader = useLoader({ key: 'post', variables: params.postId, }) diff --git a/docs/guide/preloading.md b/docs/guide/preloading.md index 9b6bb7f355..9dea97947e 100644 --- a/docs/guide/preloading.md +++ b/docs/guide/preloading.md @@ -36,7 +36,7 @@ You can also set the `preloadDelay` prop on individual `` components to ov ## Preloading with Data Loaders -Preloading is most useful when combined with your favorite data loading library. To make this easier, the `onLoad` route option function receives a `preload` boolean denoting whether the route is being preloaded or not. This allows you to load data differently depending on whether the user is navigating to the route or preloading it. +Preloading is most useful when combined with your favorite data loading library. To make this easier, the `loader` route option function receives a `preload` boolean denoting whether the route is being preloaded or not. This allows you to load data differently depending on whether the user is navigating to the route or preloading it. Here's an example using TanStack Loaders: @@ -47,7 +47,7 @@ const postsRoute = new Route({ getParentRoute: () => rootRoute, path: 'posts', component: PostsComponent, - onLoad: async ({ preload }) => { + loader: async ({ preload }) => { postsLoader.load({ preload }) }, }) diff --git a/docs/guide/route-paths.md b/docs/guide/route-paths.md index 7e08a23ccb..9474927897 100644 --- a/docs/guide/route-paths.md +++ b/docs/guide/route-paths.md @@ -142,7 +142,7 @@ A 404 / non-matching route is really just a fancy name for a [Splat / Catch-All] Pathless layout routes are routes that do not have a `path` and instead an `id` to uniquely identify them. Pathless layout routes do not use path segments from the URL pathname, nor do they add path segments to it during linking. They can be used to: - Wrap child routes with a layout component -- Enforce an `onLoad` requirement before displaying any child routes +- Enforce an `loader` requirement before displaying any child routes - Validate and provide search params to child routes - Provide fallbacks for error components or pending elements to child routes - Provide shared context to all child routes diff --git a/docs/guide/router-context.md b/docs/guide/router-context.md index 9fee6cdd7b..05cf9f55a4 100644 --- a/docs/guide/router-context.md +++ b/docs/guide/router-context.md @@ -62,7 +62,7 @@ const userRoute = Route({ getRootRoute: () => rootRoute, path: 'todos', component: Todos, - onLoad: ({ context }) => { + loader: ({ context }) => { await todosLoader.load({ variables: { user: context.user.id } }) }, }) @@ -100,7 +100,7 @@ const userRoute = Route({ getRootRoute: () => rootRoute, path: 'todos', component: Todos, - onLoad: ({ context }) => { + loader: ({ context }) => { await context.queryClient.ensureQueryData({ queryKey: ['todos', { userId: user.id }], queryFn: fetchTodos, @@ -140,7 +140,7 @@ const userRoute = Route({ bar: true, } } - onLoad: ({ context }) => { + loader: ({ context }) => { context.foo // true context.bar // true }, @@ -166,7 +166,7 @@ export const postIdRoute = new Route({ getTitle: () => `${loaderInstance.state.data?.title} | Post`, } }, - onLoad: async ({ params: { postId }, preload, context, routeContext }) => + loader: async ({ params: { postId }, preload, context, routeContext }) => routeContext.loaderInstance.load({ variables: postId, preload, diff --git a/docs/guide/search-params.md b/docs/guide/search-params.md index 89d1024152..34603f6924 100644 --- a/docs/guide/search-params.md +++ b/docs/guide/search-params.md @@ -187,7 +187,7 @@ const allProductsRoute = new Route({ getParentRoute: () => shopRoute, path: 'products', validateSearch: productSearchSchema, - onLoad: ({ search }) => { + loader: ({ search }) => { search // ^? ProductSearch ✅ }, @@ -214,7 +214,7 @@ const allProductsRoute = new Route({ const productRoute = new Route({ getParentRoute: () => allProductsRoute, path: ':productId', - onLoad: ({ search }) => { + loader: ({ search }) => { search // ^? ProductSearch ✅ }, diff --git a/examples/react/basic/src/main.tsx b/examples/react/basic/src/main.tsx index cba75a6d2a..33cccd473d 100644 --- a/examples/react/basic/src/main.tsx +++ b/examples/react/basic/src/main.tsx @@ -5,21 +5,19 @@ import { RouterProvider, Router, Link, - useParams, RootRoute, Route, ErrorComponent, - createHashHistory, - useMatch, } from '@tanstack/router' -import { TanStackRouterDevtools } from '../../../../packages/router-devtools/build/types' +import { TanStackRouterDevtools } from '@tanstack/router-devtools' import axios from 'axios' import { LoaderClient, Loader, LoaderClientProvider, - useLoaderInstance, + useLoader, } from '@tanstack/react-loaders' +import { e } from 'vitest/dist/index-40ebba2b' type PostType = { id: string @@ -27,15 +25,19 @@ type PostType = { body: string } -const fetchPosts = async () => { +declare function server$ any>( + fn: TFunc, +): () => ReturnType + +const fetchPosts = server$(async () => { console.log('Fetching posts...') await new Promise((r) => setTimeout(r, 500)) return axios .get('https://jsonplaceholder.typicode.com/posts') .then((r) => r.data.slice(0, 10)) -} +}) -const fetchPost = async (postId: string) => { +const fetchPost = server$(async (postId: string) => { console.log(`Fetching post with id ${postId}...`) await new Promise((r) => setTimeout(r, 500)) const post = await axios @@ -43,11 +45,11 @@ const fetchPost = async (postId: string) => { .then((r) => r.data) if (!post) { - throw new NotFoundError(postId) + throw new NotFoundError(`Post with id "${postId}" not found!`) } return post -} +}) const postsLoader = new Loader({ fn: fetchPosts, @@ -124,12 +126,18 @@ const indexRoute = new Route({ const postsRoute = new Route({ getParentRoute: () => rootRoute, path: 'posts', - onLoad: async ({ context }) => context.loaderClient.loaders.posts.load(), - component: () => { - const match = useMatch({ from: postsRoute.id }) - const postsLoader = useLoaderInstance({ - loader: match.context.loaderClient.loaders.posts, - }) + loader: async ({ context }) => { + const postsLoader = context.loaderClient.loaders.posts + + await postsLoader.load() + + return () => + useLoader({ + loader: postsLoader, + }) + }, + component: ({ useLoader }) => { + const postsLoader = useLoader()() return (
@@ -167,39 +175,34 @@ const postsIndexRoute = new Route({ component: () =>
Select a post.
, }) -class NotFoundError extends Error { - data: string - constructor(public postId: string) { - super(`Post with id "${postId}" not found!`) - this.data = postId - } -} +class NotFoundError extends Error {} const postRoute = new Route({ getParentRoute: () => postsRoute, path: '$postId', - onLoad: async ({ context: { loaderClient }, params: { postId } }) => { - await loaderClient.loaders.post.load({ + loader: async ({ context: { loaderClient }, params: { postId } }) => { + const postLoader = loaderClient.loaders.post + await postLoader.load({ variables: postId, }) - // Return a hook! + // Return a curried hook! return () => - useLoaderInstance({ - loader: loaderClient.loaders.post, + useLoader({ + loader: postLoader, variables: postId, }) }, errorComponent: ({ error }) => { if (error instanceof NotFoundError) { - return
Post with id "{error.data}" found!
+ return
{error.message}
} return }, component: () => { const { - state: { data }, + state: { data: post }, } = postRoute.useLoader()() return ( diff --git a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/dashboard.tsx b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/dashboard.tsx index da39adc435..b120622050 100644 --- a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/dashboard.tsx +++ b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/dashboard.tsx @@ -1,4 +1,4 @@ -import { useLoaderInstance } from '@tanstack/react-loaders' +import { useLoader } from '@tanstack/react-loaders' import { Route } from '@tanstack/router' import * as React from 'react' import { dashboardRoute, invoicesLoader } from '.' @@ -10,7 +10,7 @@ export const dashboardIndexRoute = new Route({ }) function DashboardHome() { - const invoicesLoaderInstance = useLoaderInstance({ key: invoicesLoader.key }) + const invoicesLoaderInstance = useLoader({ key: invoicesLoader.key }) const invoices = invoicesLoaderInstance.state.data return ( diff --git a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/index.tsx b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/index.tsx index 4a0fb56b3f..c8ea583086 100644 --- a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/index.tsx +++ b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/index.tsx @@ -16,7 +16,7 @@ export const dashboardRoute = new Route({ getParentRoute: () => rootRoute, path: 'dashboard', component: Dashboard, - onLoad: ({ preload }) => invoicesLoader.load({ preload }), + loader: ({ preload }) => invoicesLoader.load({ preload }), }) function Dashboard() { diff --git a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/invoices/index.tsx b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/invoices/index.tsx index 8d233f3a54..4e23d7cf71 100644 --- a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/invoices/index.tsx +++ b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/invoices/index.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { Spinner } from '../../../components/Spinner' import { dashboardRoute, invoicesLoader } from '..' import { invoiceRoute, updateInvoiceAction } from './invoice' -import { useLoaderInstance } from '@tanstack/react-loaders' +import { useLoader } from '@tanstack/react-loaders' import { useAction } from '@tanstack/react-actions' import { createInvoiceAction } from './invoices' @@ -14,7 +14,7 @@ export const invoicesRoute = new Route({ }) function Invoices() { - const invoicesLoaderInstance = useLoaderInstance({ + const invoicesLoaderInstance = useLoader({ key: invoicesLoader.key, }) const invoices = invoicesLoaderInstance.state.data diff --git a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/invoices/invoice.tsx b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/invoices/invoice.tsx index 156fca01af..142a804c16 100644 --- a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/invoices/invoice.tsx +++ b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/invoices/invoice.tsx @@ -10,7 +10,7 @@ import { useParams, Route, } from '@tanstack/router' -import { Loader, useLoaderInstance } from '@tanstack/react-loaders' +import { Loader, useLoader } from '@tanstack/react-loaders' import { Action, useAction } from '@tanstack/react-actions' import { invoicesLoader } from '..' @@ -53,7 +53,7 @@ export const invoiceRoute = new Route({ notes: z.string().optional(), }), component: InvoiceView, - onLoad: async ({ params: { invoiceId }, preload }) => + loader: async ({ params: { invoiceId }, preload }) => invoiceLoader.load({ variables: invoiceId, preload, @@ -62,7 +62,7 @@ export const invoiceRoute = new Route({ function InvoiceView() { const { invoiceId } = useParams({ from: invoiceRoute.id }) - const invoiceLoaderInstance = useLoaderInstance({ + const invoiceLoaderInstance = useLoader({ key: invoiceLoader.key, variables: invoiceId, }) diff --git a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/users/index.tsx b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/users/index.tsx index 5821589d43..2d0536b4da 100644 --- a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/users/index.tsx +++ b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/users/index.tsx @@ -12,7 +12,7 @@ import { z } from 'zod' import { Spinner } from '../../../components/Spinner' import { fetchUsers } from '../../../mockTodos' import { dashboardRoute } from '..' -import { Loader, useLoaderInstance } from '@tanstack/react-loaders' +import { Loader, useLoader } from '@tanstack/react-loaders' const usersViewSortBy = z.enum(['name', 'id', 'email']) export type UsersViewSortBy = z.infer @@ -47,12 +47,12 @@ export const usersRoute = new Route({ }, }), ], - onLoad: async ({ preload }) => usersLoader.load({ preload }), + loader: async ({ preload }) => usersLoader.load({ preload }), }) function Users() { const navigate = useNavigate({ from: usersRoute.id }) - const usersLoaderInstance = useLoaderInstance({ key: usersLoader.key }) + const usersLoaderInstance = useLoader({ key: usersLoader.key }) const users = usersLoaderInstance.state.data const { usersView } = useSearch({ from: usersRoute.id }) diff --git a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/users/user.tsx b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/users/user.tsx index 13e1738926..cafe355e74 100644 --- a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/users/user.tsx +++ b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/users/user.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { fetchUserById } from '../../../mockTodos' import { usersRoute } from '.' -import { Loader, useLoaderInstance } from '@tanstack/react-loaders' +import { Loader, useLoader } from '@tanstack/react-loaders' import { Route, useParams } from '@tanstack/router' import { loaderClient } from '../../../loaderClient' @@ -26,13 +26,13 @@ export const userRoute = new Route({ parseParams: ({ userId }) => ({ userId: Number(userId) }), stringifyParams: ({ userId }) => ({ userId: `${userId}` }), component: User, - onLoad: async ({ params: { userId }, preload }) => + loader: async ({ params: { userId }, preload }) => userLoader.load({ variables: userId, preload }), }) function User() { const { userId } = useParams({ from: userRoute.id }) - const userLoaderInstance = useLoaderInstance({ + const userLoaderInstance = useLoader({ key: userLoader.key, variables: userId, }) diff --git a/examples/react/kitchen-sink-multi-file/src/routes/layout/index.tsx b/examples/react/kitchen-sink-multi-file/src/routes/layout/index.tsx index 8b7adee826..78aabbb477 100644 --- a/examples/react/kitchen-sink-multi-file/src/routes/layout/index.tsx +++ b/examples/react/kitchen-sink-multi-file/src/routes/layout/index.tsx @@ -1,4 +1,4 @@ -import { Loader, useLoaderInstance } from '@tanstack/react-loaders' +import { Loader, useLoader } from '@tanstack/react-loaders' import { Outlet, Route } from '@tanstack/router' import * as React from 'react' import { fetchRandomNumber } from '../../mockTodos' @@ -17,7 +17,7 @@ export const layoutRoute = new Route({ getParentRoute: () => rootRoute, id: 'layout', component: LayoutWrapper, - onLoad: async () => { + loader: async () => { return loaderDelayFn(() => { return { random: Math.random(), @@ -27,7 +27,7 @@ export const layoutRoute = new Route({ }) function LayoutWrapper() { - const loaderInstance = useLoaderInstance({ key: randomIdLoader.key }) + const loaderInstance = useLoader({ key: randomIdLoader.key }) const random = loaderInstance.state.data return ( diff --git a/examples/react/with-astro-ssr/.netlify/functions-internal/chunks/Hydrate.f13f5eaa.mjs b/examples/react/with-astro-ssr/.netlify/functions-internal/chunks/Hydrate.f13f5eaa.mjs index 3ff8926359..44f9ddbaef 100644 --- a/examples/react/with-astro-ssr/.netlify/functions-internal/chunks/Hydrate.f13f5eaa.mjs +++ b/examples/react/with-astro-ssr/.netlify/functions-internal/chunks/Hydrate.f13f5eaa.mjs @@ -1,6 +1,6 @@ -import * as React from 'react'; -import jsesc from 'jsesc'; -import { jsxs, Fragment as Fragment$1, jsx } from 'react/jsx-runtime'; +import * as React from 'react' +import jsesc from 'jsesc' +import { jsxs, Fragment as Fragment$1, jsx } from 'react/jsx-runtime' /** * Copyright (C) 2017-present by Andrea Giammarchi - @WebReflection @@ -24,17 +24,17 @@ import { jsxs, Fragment as Fragment$1, jsx } from 'react/jsx-runtime'; * THE SOFTWARE. */ -const {replace} = ''; -const ca = /[&<>'"]/g; +const { replace } = '' +const ca = /[&<>'"]/g const esca = { '&': '&', '<': '<', '>': '>', "'": ''', - '"': '"' -}; -const pe = m => esca[m]; + '"': '"', +} +const pe = (m) => esca[m] /** * Safely escape HTML entities such as `&`, `<`, `>`, `"`, and `'`. @@ -43,410 +43,456 @@ const pe = m => esca[m]; * the input type is unexpected, except for boolean and numbers, * converted as string. */ -const escape = es => replace.call(es, ca, pe); +const escape = (es) => replace.call(es, ca, pe) -const escapeHTML = escape; +const escapeHTML = escape class HTMLString extends String { get [Symbol.toStringTag]() { - return "HTMLString"; + return 'HTMLString' } } const markHTMLString = (value) => { if (value instanceof HTMLString) { - return value; + return value } - if (typeof value === "string") { - return new HTMLString(value); + if (typeof value === 'string') { + return new HTMLString(value) } - return value; -}; + return value +} function isHTMLString(value) { - return Object.prototype.toString.call(value) === "[object HTMLString]"; + return Object.prototype.toString.call(value) === '[object HTMLString]' } -const AstroJSX = "astro:jsx"; -const Empty = Symbol("empty"); -const toSlotName = (slotAttr) => slotAttr; +const AstroJSX = 'astro:jsx' +const Empty = Symbol('empty') +const toSlotName = (slotAttr) => slotAttr function isVNode(vnode) { - return vnode && typeof vnode === "object" && vnode[AstroJSX]; + return vnode && typeof vnode === 'object' && vnode[AstroJSX] } function transformSlots(vnode) { - if (typeof vnode.type === "string") - return vnode; - const slots = {}; + if (typeof vnode.type === 'string') return vnode + const slots = {} if (isVNode(vnode.props.children)) { - const child = vnode.props.children; - if (!isVNode(child)) - return; - if (!("slot" in child.props)) - return; - const name = toSlotName(child.props.slot); - slots[name] = [child]; - slots[name]["$$slot"] = true; - delete child.props.slot; - delete vnode.props.children; + const child = vnode.props.children + if (!isVNode(child)) return + if (!('slot' in child.props)) return + const name = toSlotName(child.props.slot) + slots[name] = [child] + slots[name]['$$slot'] = true + delete child.props.slot + delete vnode.props.children } if (Array.isArray(vnode.props.children)) { - vnode.props.children = vnode.props.children.map((child) => { - if (!isVNode(child)) - return child; - if (!("slot" in child.props)) - return child; - const name = toSlotName(child.props.slot); - if (Array.isArray(slots[name])) { - slots[name].push(child); - } else { - slots[name] = [child]; - slots[name]["$$slot"] = true; - } - delete child.props.slot; - return Empty; - }).filter((v) => v !== Empty); + vnode.props.children = vnode.props.children + .map((child) => { + if (!isVNode(child)) return child + if (!('slot' in child.props)) return child + const name = toSlotName(child.props.slot) + if (Array.isArray(slots[name])) { + slots[name].push(child) + } else { + slots[name] = [child] + slots[name]['$$slot'] = true + } + delete child.props.slot + return Empty + }) + .filter((v) => v !== Empty) } - Object.assign(vnode.props, slots); + Object.assign(vnode.props, slots) } function markRawChildren(child) { - if (typeof child === "string") - return markHTMLString(child); - if (Array.isArray(child)) - return child.map((c) => markRawChildren(c)); - return child; + if (typeof child === 'string') return markHTMLString(child) + if (Array.isArray(child)) return child.map((c) => markRawChildren(c)) + return child } function transformSetDirectives(vnode) { - if (!("set:html" in vnode.props || "set:text" in vnode.props)) - return; - if ("set:html" in vnode.props) { - const children = markRawChildren(vnode.props["set:html"]); - delete vnode.props["set:html"]; - Object.assign(vnode.props, { children }); - return; + if (!('set:html' in vnode.props || 'set:text' in vnode.props)) return + if ('set:html' in vnode.props) { + const children = markRawChildren(vnode.props['set:html']) + delete vnode.props['set:html'] + Object.assign(vnode.props, { children }) + return } - if ("set:text" in vnode.props) { - const children = vnode.props["set:text"]; - delete vnode.props["set:text"]; - Object.assign(vnode.props, { children }); - return; + if ('set:text' in vnode.props) { + const children = vnode.props['set:text'] + delete vnode.props['set:text'] + Object.assign(vnode.props, { children }) + return } } function createVNode(type, props) { const vnode = { - [Renderer]: "astro:jsx", + [Renderer]: 'astro:jsx', [AstroJSX]: true, type, - props: props ?? {} - }; - transformSetDirectives(vnode); - transformSlots(vnode); - return vnode; + props: props ?? {}, + } + transformSetDirectives(vnode) + transformSlots(vnode) + return vnode } -var idle_prebuilt_default = `(self.Astro=self.Astro||{}).idle=t=>{const e=async()=>{await(await t())()};"requestIdleCallback"in window?window.requestIdleCallback(e):setTimeout(e,200)},window.dispatchEvent(new Event("astro:idle"));`; +var idle_prebuilt_default = `(self.Astro=self.Astro||{}).idle=t=>{const e=async()=>{await(await t())()};"requestIdleCallback"in window?window.requestIdleCallback(e):setTimeout(e,200)},window.dispatchEvent(new Event("astro:idle"));` -var load_prebuilt_default = `(self.Astro=self.Astro||{}).load=a=>{(async()=>await(await a())())()},window.dispatchEvent(new Event("astro:load"));`; +var load_prebuilt_default = `(self.Astro=self.Astro||{}).load=a=>{(async()=>await(await a())())()},window.dispatchEvent(new Event("astro:load"));` -var media_prebuilt_default = `(self.Astro=self.Astro||{}).media=(s,a)=>{const t=async()=>{await(await s())()};if(a.value){const e=matchMedia(a.value);e.matches?t():e.addEventListener("change",t,{once:!0})}},window.dispatchEvent(new Event("astro:media"));`; +var media_prebuilt_default = `(self.Astro=self.Astro||{}).media=(s,a)=>{const t=async()=>{await(await s())()};if(a.value){const e=matchMedia(a.value);e.matches?t():e.addEventListener("change",t,{once:!0})}},window.dispatchEvent(new Event("astro:media"));` -var only_prebuilt_default = `(self.Astro=self.Astro||{}).only=t=>{(async()=>await(await t())())()},window.dispatchEvent(new Event("astro:only"));`; +var only_prebuilt_default = `(self.Astro=self.Astro||{}).only=t=>{(async()=>await(await t())())()},window.dispatchEvent(new Event("astro:only"));` -var visible_prebuilt_default = `(self.Astro=self.Astro||{}).visible=(s,c,n)=>{const r=async()=>{await(await s())()};let i=new IntersectionObserver(e=>{for(const t of e)if(!!t.isIntersecting){i.disconnect(),r();break}});for(let e=0;e{const r=async()=>{await(await s())()};let i=new IntersectionObserver(e=>{for(const t of e)if(!!t.isIntersecting){i.disconnect(),r();break}});for(let e=0;et,1:t=>JSON.parse(t,o),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,o)),5:t=>new Set(JSON.parse(t,o)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(JSON.parse(t)),9:t=>new Uint16Array(JSON.parse(t)),10:t=>new Uint32Array(JSON.parse(t))},o=(t,s)=>{if(t===""||!Array.isArray(s))return s;const[e,n]=s;return e in c?c[e](n):void 0};customElements.get("astro-island")||customElements.define("astro-island",(l=class extends HTMLElement{constructor(){super(...arguments);this.hydrate=()=>{if(!this.hydrator||this.parentElement&&this.parentElement.closest("astro-island[ssr]"))return;const s=this.querySelectorAll("astro-slot"),e={},n=this.querySelectorAll("template[data-astro-template]");for(const r of n){const i=r.closest(this.tagName);!i||!i.isSameNode(this)||(e[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(const r of s){const i=r.closest(this.tagName);!i||!i.isSameNode(this)||(e[r.getAttribute("name")||"default"]=r.innerHTML)}const a=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),o):{};this.hydrator(this)(this.Component,a,e,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),window.removeEventListener("astro:hydrate",this.hydrate),window.dispatchEvent(new CustomEvent("astro:hydrate"))}}connectedCallback(){!this.hasAttribute("await-children")||this.firstChild?this.childrenConnectedCallback():new MutationObserver((s,e)=>{e.disconnect(),this.childrenConnectedCallback()}).observe(this,{childList:!0})}async childrenConnectedCallback(){window.addEventListener("astro:hydrate",this.hydrate);let s=this.getAttribute("before-hydration-url");s&&await import(s),this.start()}start(){const s=JSON.parse(this.getAttribute("opts")),e=this.getAttribute("client");if(Astro[e]===void 0){window.addEventListener(\`astro:\${e}\`,()=>this.start(),{once:!0});return}Astro[e](async()=>{const n=this.getAttribute("renderer-url"),[a,{default:r}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),i=this.getAttribute("component-export")||"default";if(!i.includes("."))this.Component=a[i];else{this.Component=a;for(const d of i.split("."))this.Component=this.Component[d]}return this.hydrator=r,this.hydrate},s,this)}attributeChangedCallback(){this.hydrator&&this.hydrate()}},l.observedAttributes=["props"],l))}`; +var astro_island_prebuilt_default = `var l;{const c={0:t=>t,1:t=>JSON.parse(t,o),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,o)),5:t=>new Set(JSON.parse(t,o)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(JSON.parse(t)),9:t=>new Uint16Array(JSON.parse(t)),10:t=>new Uint32Array(JSON.parse(t))},o=(t,s)=>{if(t===""||!Array.isArray(s))return s;const[e,n]=s;return e in c?c[e](n):void 0};customElements.get("astro-island")||customElements.define("astro-island",(l=class extends HTMLElement{constructor(){super(...arguments);this.hydrate=()=>{if(!this.hydrator||this.parentElement&&this.parentElement.closest("astro-island[ssr]"))return;const s=this.querySelectorAll("astro-slot"),e={},n=this.querySelectorAll("template[data-astro-template]");for(const r of n){const i=r.closest(this.tagName);!i||!i.isSameNode(this)||(e[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(const r of s){const i=r.closest(this.tagName);!i||!i.isSameNode(this)||(e[r.getAttribute("name")||"default"]=r.innerHTML)}const a=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),o):{};this.hydrator(this)(this.Component,a,e,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),window.removeEventListener("astro:hydrate",this.hydrate),window.dispatchEvent(new CustomEvent("astro:hydrate"))}}connectedCallback(){!this.hasAttribute("await-children")||this.firstChild?this.childrenConnectedCallback():new MutationObserver((s,e)=>{e.disconnect(),this.childrenConnectedCallback()}).observe(this,{childList:!0})}async childrenConnectedCallback(){window.addEventListener("astro:hydrate",this.hydrate);let s=this.getAttribute("before-hydration-url");s&&await import(s),this.start()}start(){const s=JSON.parse(this.getAttribute("opts")),e=this.getAttribute("client");if(Astro[e]===void 0){window.addEventListener(\`astro:\${e}\`,()=>this.start(),{once:!0});return}Astro[e](async()=>{const n=this.getAttribute("renderer-url"),[a,{default:r}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),i=this.getAttribute("component-export")||"default";if(!i.includes("."))this.Component=a[i];else{this.Component=a;for(const d of i.split("."))this.Component=this.Component[d]}return this.hydrator=r,this.hydrate},s,this)}attributeChangedCallback(){this.hydrator&&this.hydrate()}},l.observedAttributes=["props"],l))}` function determineIfNeedsHydrationScript(result) { if (result._metadata.hasHydrationScript) { - return false; + return false } - return result._metadata.hasHydrationScript = true; + return (result._metadata.hasHydrationScript = true) } const hydrationScripts = { idle: idle_prebuilt_default, load: load_prebuilt_default, only: only_prebuilt_default, media: media_prebuilt_default, - visible: visible_prebuilt_default -}; + visible: visible_prebuilt_default, +} function determinesIfNeedsDirectiveScript(result, directive) { if (result._metadata.hasDirectives.has(directive)) { - return false; + return false } - result._metadata.hasDirectives.add(directive); - return true; + result._metadata.hasDirectives.add(directive) + return true } function getDirectiveScriptText(directive) { if (!(directive in hydrationScripts)) { - throw new Error(`Unknown directive: ${directive}`); + throw new Error(`Unknown directive: ${directive}`) } - const directiveScriptText = hydrationScripts[directive]; - return directiveScriptText; + const directiveScriptText = hydrationScripts[directive] + return directiveScriptText } function getPrescripts(type, directive) { switch (type) { - case "both": - return `