diff --git a/docs/guide/data-loading.md b/docs/guide/data-loading.md index 7cea4b38d1..9d06a0ba76 100644 --- a/docs/guide/data-loading.md +++ b/docs/guide/data-loading.md @@ -64,8 +64,7 @@ import { Route } from '@tanstack/router' import { Loader, useLoader } from '@tanstack/react-loaders' const postsLoader = new Loader({ - key: 'posts', - loader: async (params) => { + fn: async (params) => { const res = await fetch(`/api/posts`) if (!res.ok) throw new Error('Failed to fetch posts') return res.json() @@ -75,13 +74,16 @@ const postsLoader = new Loader({ const postsRoute = new Route({ getParentPath: () => rootRoute, path: 'posts', - async loader() { - // Wait for the loader to finish + fn: async () => { + // Ensure our loader is loaded await postsLoader.load() + + // Return a hook fn we can use in our component + return () => useLoader({ loader: postsLoader }) }, - component: () => { - // Access the loader's data, in this case with the useLoader hook - const posts = useLoader({ loader: postsLoader }) + component: ({ useLoader }) => { + // Access the hook we made in the loader function (and call it) + const posts = useLoader()() return
...
}, @@ -111,9 +113,8 @@ import { Route } from '@tanstack/router' import { Loader, useLoader } from '@tanstack/react-loaders' const postLoader = new Loader({ - key: 'post', // Accept a postId string variable - loader: async (postId: string) => { + fn: async (postId: string) => { const res = await fetch(`/api/posts/${postId}`) if (!res.ok) throw new Error('Failed to fetch posts') return res.json() @@ -124,11 +125,13 @@ const postRoute = new Route({ getParentPath: () => postsRoute, path: '$postId', async loader({ params }) { + // Load our loader await postLoader.load({ variables: params.postId }) + // Return a hook fn we can use in our component + return () => useLoader({ loader: postLoader, variables: params.postId }) }, - component: () => { - const { postId } = useParams({ from: postRoute.id }) - const posts = useLoader({ loader: postLoader, variables: postId }) + component: ({ useLoader }) => { + const posts = useLoader()() return
...
}, @@ -144,9 +147,8 @@ import { Route } from '@tanstack/router' import { Loader, useLoader } from '@tanstack/react-loaders' const postsLoader = new Loader({ - key: 'posts', // Accept a page number variable - loader: async (pageIndex: number) => { + fn: async (pageIndex: number) => { const res = await fetch(`/api/posts?page=${pageIndex}`) if (!res.ok) throw new Error('Failed to fetch posts') return res.json() @@ -160,14 +162,13 @@ const postsRoute = new Route({ pageIndex: z.number().int().nonnegative().catch(0), }), async loader({ search }) { + // Load our loader await postsLoader.load({ variables: search.pageIndex }) + // Return a hook fn we can use in our component + return () => useLoader({ loader: postsLoader, variables: search.pageIndex }) }, - component: () => { - const search = useSearchParams({ from: postsRoute.id }) - const posts = useLoader({ - loader: postsLoader, - variables: search.pageIndex, - }) + component: ({ useLoader }) => { + const posts = useLoader()() return
...
}, @@ -176,7 +177,7 @@ const postsRoute = new Route({ ## Using Context -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. +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 TanStack Loader `loaderClient` instance 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. @@ -185,8 +186,7 @@ import { Route } from '@tanstack/router' import { Loader, useLoader } from '@tanstack/react-loaders' const postsLoader = new Loader({ - key: 'posts', - loader: async () => { + fn: async () => { const res = await fetch(`/api/posts`) if (!res.ok) throw new Error('Failed to fetch posts') return res.json() @@ -194,7 +194,7 @@ const postsLoader = new Loader({ }) const loaderClient = new LoaderClient({ - getLoaders: () => [postsLoader], + getLoaders: () => ({ postsLoader }), }) // Use RootRoute's special `withRouterContext` method to require a specific type @@ -205,17 +205,19 @@ const rootRoute = RootRoute.withRouterContext<{ loaderClient: typeof loaderClient }>()() -// Notice how our postsRoute reference context to get the loader client +// Notice how our postsRoute references context to get the loader client // This can be a powerful tool for dependency injection across your router // and routes. const postsRoute = new Route({ getParentPath: () => rootRoute, path: 'posts', async loader({ context }) { - await context.loaderClient.getLoader({ key: 'posts' }).load() + const { postsLoader } = context.loaderClient + await postsLoader.load() + return () => useLoader({ loader: postsLoader }) }, - component: () => { - const posts = useLoader({ key: 'posts' }) + component: ({ useLoader }) => { + const posts = useLoader()() return
...
}, @@ -241,9 +243,8 @@ import { Route } from '@tanstack/router' import { Loader, useLoader } from '@tanstack/react-loaders' const postsLoader = new Loader({ - key: 'posts', // Accept a page number variable - loader: async (pageIndex: number, { signal }) => { + fn: async (pageIndex: number, { signal }) => { const res = await fetch(`/api/posts?page=${pageIndex}`, { signal }) if (!res.ok) throw new Error('Failed to fetch posts') return res.json() @@ -256,26 +257,26 @@ const postsRoute = new Route({ async loader({ signal }) { // Pass the route's signal to the loader await postsLoader.load({ signal }) + return () => useLoader({ loader: postsLoader }) }, - component: () => { - const posts = useLoader({ loader: postsLoader }) + component: ({ useLoader }) => { + const posts = useLoader()() return
...
}, }) ``` -## Using the `prefetch` flag +## Using the `preload` 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: +The `preload` property of the `loader` function is a boolean which is `true` when the route is being loaded via a preload action. Some data loading libraries may handle preloading differently than a standard fetch, so you may want to pass `preload` 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 `preload` flag: ```tsx import { Route } from '@tanstack/router' import { Loader, useLoader } from '@tanstack/react-loaders' const postsLoader = new Loader({ - key: 'posts', - loader: async () => { + fn: async () => { const res = await fetch(`/api/posts?page=${pageIndex}`) if (!res.ok) throw new Error('Failed to fetch posts') return res.json() @@ -285,19 +286,20 @@ const postsLoader = new Loader({ const postsRoute = new Route({ getParentPath: () => rootRoute, path: 'posts', - async loader({ prefetch }) { - // Pass the route's prefetch to the loader - await postsLoader.load({ prefetch }) + async loader({ preload }) { + // Pass the route's preload to the loader + await postsLoader.load({ preload }) + return () => useLoader({ loader: postsLoader }) }, - component: () => { - const posts = useLoader({ loader: postsLoader }) + component: ({ useLoader }) => { + const posts = useLoader()() return
...
}, }) ``` -> 🧠 TanStack Loaders uses the `prefetch` flag to determine cache freshness vs non-prefetch calls and also to determine if the global `isLoading` or `isPrefetching` flags should be incremented or not. +> 🧠 TanStack Loaders uses the `preload` flag to determine cache freshness vs non-preload calls and also to determine if the global `isLoading` or `isPrefetching` flags should be incremented or not. ## Learn more about TanStack Loaders/Actions! diff --git a/docs/guide/data-mutations.md b/docs/guide/data-mutations.md index d758df683a..77a9bd727e 100644 --- a/docs/guide/data-mutations.md +++ b/docs/guide/data-mutations.md @@ -31,7 +31,7 @@ Similar to data fetching, mutation state isn't a one-size-fits-all solution, so ## TanStack Actions -Just like a fresh Zelda game, we would never send you into the wild without a sword. We've created an extremely lightweight, framework agnostic action/mutation library called TanStack Actions that works really well with Router. It's a great place to start if you're not already using one of the more complex (but more powerful) tools above. +Just like a fresh Zelda game, we would never send you into the wild without a sword (fine... BotW and TotK bend this rule slightly, but since they're the greatest games ever created, we'll let the lore slide a bit). We've created an extremely lightweight, framework agnostic action/mutation library called TanStack Actions that works really well with Router. It's a great place to start if you're not already using one of the more complex (but more powerful) tools above. ## What are data mutations? @@ -45,8 +45,7 @@ Let's write a data mutation that will update a post on a server. We'll use TanSt import { Action } from '@tanstack/actions' const updatePostAction = new Action({ - name: 'updatePost', - async action(post: Post) { + fn: async (post: Post) => { const response = await fetch(`/api/posts/${post.id}`, { method: 'PATCH', body: JSON.stringify(post), @@ -66,16 +65,9 @@ Now that we have our action, we can use it in our component. We'll use the `useA ```tsx import { useAction } from '@tanstack/react-actions' -function PostEditor() { - const params = useParams({ from: postEditRoute.id }) - const postLoader = useLoader({ - key: 'post', - variables: params.postId, - }) - - const [postDraft, setPostDraft] = useState(() => postLoader.state.data) +function PostEditor({ post }: { post: Post }) { + const [postDraft, setPostDraft] = useState(() => post) const updatePost = useAction({ action: updatePostAction }) - const latestPostSubmission = updatePost.state.latestSubmission return ( @@ -98,8 +90,7 @@ So how does my data loader get the updated data? **Invalidation**. When you muta import { Action } from '@tanstack/actions' const updatePostAction = new Action({ - name: 'updatePost', - async action(post: Post) { + fn: async (post: Post) => { //... }, onEachSuccess: () => { @@ -113,38 +104,36 @@ const updatePostAction = new Action({ ## Invalidating specific data -Again, we'll assume we're using TanStack Actions here, but it's also possible to use the action submission state to invalidate specific data. Let's update our action to invalidate a specific post. +Again, we'll assume we're using TanStack Actions here, but it's also possible to use the action submission state to invalidate specific data. Let's update our action to invalidate a specific post loader instance using the loader's `invalidateInstance` method. ```tsx import { Action } from '@tanstack/actions' const updatePostAction = new Action({ - name: 'updatePost', - async action(post: Post) { + fn: async (post: Post) => { //... }, onEachSuccess: (submission) => { // Use the submission payload to invalidate the specific post const post = submission.payload - postsLoader.invalidate({ variables: post.id }) + postsLoader.invalidateInstance({ variables: post.id }) }, }) ``` ## Invalidating entire data sets -It's very common to invalidate an entire subset of data based on a query key when some subset of that data changes e.g. Refetching all posts when a single post is edited. One of the best reasons to do this is that you can never really be sure of the side-effects a mutation will have on server-side data. It could remove/add elements, reorder them, or change their inclusion in specific filtered lists. TanStack Loaders comes with the `invalidateAll` method to invalidate all data for a given query key. +It's very common to invalidate an entire subset of data based on hierarchy when some subset of that data changes e.g. Refetching all posts when a single post is edited. One of the best reasons to do this is that you can never really be sure of the side-effects a mutation will have on server-side data. It could remove/add elements, reorder them, or change their inclusion in specific filtered lists. TanStack Loaders comes with the `invalidate` method to invalidate all data for a given loader. ```tsx import { Action } from '@tanstack/actions' const updatePostAction = new Action({ - name: 'updatePost', - async action(post: Post) { + fn: async (post: Post) => { //... }, onEachSuccess: (submission) => { - postsLoader.invalidateAll() + postsLoader.invalidate() }, }) ``` @@ -156,14 +145,8 @@ When mutations are in flight, successful, or failed, it's important to display t ```tsx import { useAction } from '@tanstack/react-actions' -function PostEditor() { - const params = useParams({ from: postEditRoute.id }) - const postLoader = useLoader({ - key: 'post', - variables: params.postId, - }) - - const [postDraft, setPostDraft] = useState(() => postLoader.state.data) +function PostEditor({ post }: { post: Post }) { + const [postDraft, setPostDraft] = useState(() => post) const updatePost = useAction({ action: updatePostAction }) // Get the latest submission @@ -218,12 +201,11 @@ This is a great place to reset your old mutation/actions states. We'll use TanSt ```tsx const updatePostAction = new Action({ - name: 'updatePost', - async action(post: Post) { + fn: async (post: Post) => { //... }, onEachSuccess: (submission) => { - postsLoader.invalidateAll() + postsLoader.invalidate() }, }) diff --git a/examples/react/kitchen-sink-multi-file/src/actionClient.tsx b/examples/react/kitchen-sink-multi-file/src/actionClient.tsx index 35d7d6fa76..73f8dc4b21 100644 --- a/examples/react/kitchen-sink-multi-file/src/actionClient.tsx +++ b/examples/react/kitchen-sink-multi-file/src/actionClient.tsx @@ -3,7 +3,10 @@ import { updateInvoiceAction } from './routes/dashboard/invoices/invoice' import { createInvoiceAction } from './routes/dashboard/invoices/invoices' export const actionClient = new ActionClient({ - getActions: () => [createInvoiceAction, updateInvoiceAction], + getActions: () => ({ + createInvoiceAction, + updateInvoiceAction, + }), }) declare module '@tanstack/react-actions' { 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 48df4669f7..803ff8e25e 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 @@ -32,8 +32,7 @@ export const invoiceLoader = new Loader({ }) export const updateInvoiceAction = new Action({ - key: 'updateInvoice', - action: patchInvoice, + fn: patchInvoice, onEachSuccess: async ({ payload }) => { await invoiceLoader.invalidateInstance({ variables: payload.id, @@ -70,7 +69,7 @@ export const invoiceRoute = new Route({ state: { data: invoice }, } = useLoader()() const search = useSearch() - const action = useAction({ key: updateInvoiceAction.key }) + const action = useAction({ action: updateInvoiceAction }) const navigate = useNavigate() const [notes, setNotes] = React.useState(search.notes ?? ``) diff --git a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/invoices/invoices.tsx b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/invoices/invoices.tsx index ff562d3e7f..0dbc4096d5 100644 --- a/examples/react/kitchen-sink-multi-file/src/routes/dashboard/invoices/invoices.tsx +++ b/examples/react/kitchen-sink-multi-file/src/routes/dashboard/invoices/invoices.tsx @@ -6,8 +6,7 @@ import { Action, useAction } from '@tanstack/react-actions' import { Route } from '@tanstack/router' export const createInvoiceAction = new Action({ - key: 'createInvoice', - action: postInvoice, + fn: postInvoice, onEachSuccess: async () => { await invoicesLoader.invalidate() }, @@ -17,7 +16,7 @@ export const invoicesIndexRoute = new Route({ getParentRoute: () => invoicesRoute, path: '/', component: function InvoicesHome() { - const action = useAction({ key: createInvoiceAction.key }) + const action = useAction({ action: createInvoiceAction }) return ( <> diff --git a/examples/react/kitchen-sink-single-file/src/main.tsx b/examples/react/kitchen-sink-single-file/src/main.tsx index d99d47f484..d62d7a54d5 100644 --- a/examples/react/kitchen-sink-single-file/src/main.tsx +++ b/examples/react/kitchen-sink-single-file/src/main.tsx @@ -108,16 +108,14 @@ declare module '@tanstack/react-loaders' { // Actions const createInvoiceAction = new Action({ - key: 'createInvoice', - action: postInvoice, + fn: postInvoice, onEachSuccess: async () => { await invoicesLoader.invalidate() }, }) const updateInvoiceAction = new Action({ - key: 'updateInvoice', - action: patchInvoice, + fn: patchInvoice, onEachSuccess: async ({ payload }) => { await invoiceLoader.invalidateInstance({ variables: payload.id, @@ -126,7 +124,7 @@ const updateInvoiceAction = new Action({ }) const actionClient = new ActionClient({ - getActions: () => [createInvoiceAction, updateInvoiceAction], + getActions: () => ({ createInvoiceAction, updateInvoiceAction }), }) // Register things for typesafety @@ -318,13 +316,13 @@ const invoicesRoute = new Route({ const { state: { pendingSubmissions: updateSubmissions }, } = useAction({ - key: updateInvoiceAction.key, + action: updateInvoiceAction, }) const { state: { pendingSubmissions: createSubmissions }, } = useAction({ - key: createInvoiceAction.key, + action: createInvoiceAction, }) return ( @@ -397,7 +395,7 @@ const invoicesIndexRoute = new Route({ component: () => { const { state: { latestSubmission }, - } = useAction({ key: createInvoiceAction.key }) + } = useAction({ action: createInvoiceAction }) return ( <> @@ -475,7 +473,7 @@ const invoiceRoute = new Route({ const { state: { latestSubmission }, - } = useAction({ key: updateInvoiceAction.key }) + } = useAction({ action: updateInvoiceAction }) const [notes, setNotes] = React.useState(search.notes ?? '') diff --git a/examples/react/start-kitchen-sink-single-file/src/main.tsx b/examples/react/start-kitchen-sink-single-file/src/main.tsx index d50be7b563..1c38479d62 100644 --- a/examples/react/start-kitchen-sink-single-file/src/main.tsx +++ b/examples/react/start-kitchen-sink-single-file/src/main.tsx @@ -48,58 +48,53 @@ type UsersViewSortBy = 'name' | 'id' | 'email' // Loaders const invoicesLoader = new Loader({ - key: 'invoices', - loader: async () => { + fn: async () => { console.log('Fetching invoices...') return fetchInvoices() }, }) const invoiceLoader = new Loader({ - key: 'invoice', - loader: async (invoiceId: number) => { + fn: async (invoiceId: number) => { console.log(`Fetching invoice with id ${invoiceId}...`) return fetchInvoiceById(invoiceId) }, - onAllInvalidate: async () => { - await invoicesLoader.invalidateAll() + onInvalidate: async () => { + await invoicesLoader.invalidate() }, }) const usersLoader = new Loader({ - key: 'users', - loader: async () => { + fn: async () => { console.log('Fetching users...') return fetchUsers() }, }) const userLoader = new Loader({ - key: 'user', - loader: async (userId: number) => { + fn: async (userId: number) => { console.log(`Fetching user with id ${userId}...`) return fetchUserById(userId) }, - onAllInvalidate: async () => { - await usersLoader.invalidateAll() + onInvalidate: async () => { + await usersLoader.invalidate() }, }) const randomIdLoader = new Loader({ - key: 'random', - loader: () => { + fn: () => { return fetchRandomNumber() }, }) const loaderClient = new LoaderClient({ - getLoaders: () => [ + getLoaders: () => ({ invoicesLoader, invoiceLoader, usersLoader, userLoader, randomIdLoader, - ], + }), }) // Register things for typesafety @@ -112,25 +107,23 @@ declare module '@tanstack/react-loaders' { // Actions const createInvoiceAction = new Action({ - key: 'createInvoice', - action: postInvoice, + fn: postInvoice, onEachSuccess: async () => { - await invoicesLoader.invalidateAll() + await invoicesLoader.invalidate() }, }) const updateInvoiceAction = new Action({ - key: 'updateInvoice', - action: patchInvoice, + fn: patchInvoice, onEachSuccess: async ({ payload }) => { - await invoiceLoader.invalidate({ + await invoiceLoader.invalidateInstance({ variables: payload.id, }) }, }) const actionClient = new ActionClient({ - getActions: () => [createInvoiceAction, updateInvoiceAction], + getActions: () => ({ createInvoiceAction, updateInvoiceAction }), }) // Register things for typesafety @@ -297,7 +290,7 @@ const dashboardIndexRoute = new Route({ path: '/', component: () => { const invoicesLoaderInstance = useLoader({ - key: invoicesLoader.key, + loader: invoicesLoader, }) const invoices = invoicesLoaderInstance.state.data @@ -318,7 +311,7 @@ const invoicesRoute = new Route({ path: 'invoices', component: () => { const invoicesLoaderInstance = useLoader({ - key: invoicesLoader.key, + loader: invoicesLoader, }) const invoices = invoicesLoaderInstance.state.data @@ -326,13 +319,13 @@ const invoicesRoute = new Route({ const { state: { pendingSubmissions: updateSubmissions }, } = useAction({ - key: updateInvoiceAction.key, + action: updateInvoiceAction, }) const { state: { pendingSubmissions: createSubmissions }, } = useAction({ - key: createInvoiceAction.key, + action: createInvoiceAction, }) return ( @@ -405,7 +398,7 @@ const invoicesIndexRoute = new Route({ component: () => { const { state: { latestSubmission }, - } = useAction({ key: createInvoiceAction.key }) + } = useAction({ action: createInvoiceAction }) return ( <> @@ -473,7 +466,7 @@ const invoiceRoute = new Route({ const navigate = useNavigate({ from: invoiceRoute.id }) const invoiceLoaderInstance = useLoader({ - key: invoiceLoader.key, + loader: invoiceLoader, variables: params.invoiceId, }) @@ -481,7 +474,7 @@ const invoiceRoute = new Route({ const { state: { latestSubmission }, - } = useAction({ key: updateInvoiceAction.key }) + } = useAction({ action: updateInvoiceAction }) const [notes, setNotes] = React.useState(search.notes ?? '') @@ -594,7 +587,7 @@ const usersRoute = new Route({ }), ], component: () => { - const usersLoaderInstance = useLoader({ key: usersLoader.key }) + const usersLoaderInstance = useLoader({ loader: usersLoader }) const users = usersLoaderInstance.state.data const { usersView } = useSearch({ from: usersRoute.id }) @@ -757,7 +750,7 @@ const userRoute = new Route({ const { userId } = useSearch({ from: userRoute.id }) const userLoaderInstance = useLoader({ - key: userLoader.key, + loader: userLoader, variables: userId, }) @@ -883,7 +876,7 @@ const layoutRoute = new Route({ loader: async ({ preload }) => randomIdLoader.load({ preload }), component: () => { const randomIdLoaderInstance = useLoader({ - key: randomIdLoader.key, + loader: randomIdLoader, }) return ( diff --git a/packages/actions/src/index.ts b/packages/actions/src/index.ts index e68faec257..4afac15519 100644 --- a/packages/actions/src/index.ts +++ b/packages/actions/src/index.ts @@ -17,10 +17,12 @@ export type RegisteredActions = Register extends { actionClient: ActionClient } ? TActions - : Action[] + : Record + +export type AnyAction = Action export interface ActionClientOptions< - TActions extends Action[], + TActions extends Record, > { getActions: () => TActions defaultMaxAge?: number @@ -30,18 +32,31 @@ export interface ActionClientOptions< export type ActionClientStore = Store<{ isSubmitting?: ActionSubmission[] }> + +type ResolveActions> = { + [TKey in keyof TAction]: TAction[TKey] extends Action< + infer _, + infer TVariables, + infer TData, + infer TError + > + ? Action<_, TVariables, TData, TError> + : Action +} + // A action client that tracks instances of actions by unique key like react query export class ActionClient< - TActions extends Action[] = Action[], + _TActions extends Record = Record, + TActions extends ResolveActions<_TActions> = ResolveActions<_TActions>, > { - options: ActionClientOptions - actions: Record + options: ActionClientOptions<_TActions> + actions: TActions __store: ActionClientStore state: ActionClientStore['state'] initialized = false - constructor(options: ActionClientOptions) { + constructor(options: ActionClientOptions<_TActions>) { this.options = options this.__store = new Store( {}, @@ -52,26 +67,20 @@ export class ActionClient< }, ) as ActionClientStore this.state = this.__store.state - this.actions = {} + this.actions = {} as any + this.init() } init = () => { - this.options.getActions().forEach((action) => { - action.client = this - - this.actions[action.key] = action - }) - + if (this.initialized) return + Object.entries(this.options.getActions()).forEach( + ([key, action]: [string, Action]) => { + ;(this.actions as any)[key] = action.init(key, this) + }, + ) this.initialized = true } - getAction(opts: { - key: TKey - }): ActionByKey { - if (!this.initialized) this.init() - return this.actions[opts.key as any] as any - } - clearAll = () => { Object.keys(this.actions).forEach((key) => { this.actions[key]!.clear() @@ -80,15 +89,9 @@ export class ActionClient< } export type ActionByKey< - TActions extends Action[], - TKey extends TActions[number]['__types']['key'], -> = { - [TAction in TActions[number] as number]: TAction extends { - options: ActionOptions - } - ? Action - : never -}[number] + TActions extends Record, + TKey extends keyof TActions, +> = TActions[TKey] export interface ActionOptions< TKey extends string = string, @@ -96,8 +99,7 @@ export interface ActionOptions< TResponse = unknown, TError = Error, > { - key: TKey - action: (payload: TPayload) => TResponse | Promise + fn: (payload: TPayload) => TResponse | Promise onLatestSuccess?: ActionCallback onEachSuccess?: ActionCallback onLatestError?: ActionCallback @@ -124,7 +126,7 @@ export class Action< response: TResponse error: TError } - key: TKey + key!: TKey client?: ActionClient options: ActionOptions __store: Store> @@ -146,7 +148,11 @@ export class Action< ) this.state = this.#resolveState(this.__store.state) this.options = options - this.key = options.key + } + + init = (key: TKey, client: ActionClient) => { + this.client = client + this.key = key as TKey } #resolveState = ( @@ -239,7 +245,7 @@ export class Action< } try { - const res = await this.options.action?.(submission.payload) + const res = await this.options.fn?.(submission.payload) setSubmission((s) => ({ ...s, response: res, diff --git a/packages/loaders/src/index.ts b/packages/loaders/src/index.ts index 5bd157d6b8..272b08da1d 100644 --- a/packages/loaders/src/index.ts +++ b/packages/loaders/src/index.ts @@ -61,10 +61,10 @@ type ResolveLoaders> = { // A loader client that tracks instances of loaders by unique key like react query export class LoaderClient< _TLoaders extends Record = Record, - TLoader extends ResolveLoaders<_TLoaders> = ResolveLoaders<_TLoaders>, + TLoaders extends ResolveLoaders<_TLoaders> = ResolveLoaders<_TLoaders>, > { options: LoaderClientOptions<_TLoaders> - loaders: TLoader + loaders: TLoaders loaderInstances: Record = {} __store: LoaderClientStore state: LoaderClientStore['state'] @@ -143,14 +143,14 @@ export class LoaderClient< } export type LoaderByKey< - TLoader extends Record, - TKey extends keyof TLoader, -> = TLoader[TKey] + TLoaders extends Record, + TKey extends keyof TLoaders, +> = TLoaders[TKey] export type LoaderInstanceByKey< - TLoader extends Record, - TKey extends keyof TLoader, -> = TLoader[TKey] extends Loader< + TLoaders extends Record, + TKey extends keyof TLoaders, +> = TLoaders[TKey] extends Loader< infer _, infer TVariables, infer TData, @@ -269,7 +269,6 @@ export type VariablesFn< } const visibilityChangeEvent = 'visibilitychange' -const focusEvent = 'focus' export type AnyLoader = Loader diff --git a/packages/react-actions/src/index.tsx b/packages/react-actions/src/index.tsx index 7a28420b6d..8e0e942aa0 100644 --- a/packages/react-actions/src/index.tsx +++ b/packages/react-actions/src/index.tsx @@ -27,7 +27,7 @@ export function ActionClientProvider(props: { } export function useAction< - TKey extends string, + TKey extends keyof RegisteredActions, TAction, TActionFromKey extends ActionByKey, TResolvedAction extends unknown extends TAction @@ -48,17 +48,12 @@ export function useAction< track?: (actionStore: ActionStore) => any }, ): TResolvedAction { - const allOpts = opts as typeof opts & { - action?: Action - key?: TKey - } - const actionClient = React.useContext(actionClientContext) - const action = allOpts.action ?? actionClient.getAction({ key: allOpts.key }) - - useStore(action.__store, (d) => allOpts?.track?.(d as any) ?? d) - + const optsKey = (opts as { key: string }).key + const optsAction = (opts as { action: any }).action + const action = optsAction ?? actionClient.actions[optsKey] + useStore(action.__store, (d) => opts?.track?.(d as any) ?? d) return action as any }