diff --git a/docs/config.json b/docs/config.json index d905826fb49..5ff8e8f2407 100644 --- a/docs/config.json +++ b/docs/config.json @@ -1419,6 +1419,10 @@ { "label": "Astro", "to": "framework/solid/examples/astro" + }, + { + "label": "Offline Queries and Mutations", + "to": "framework/solid/examples/offline" } ] }, diff --git a/docs/framework/react/guides/caching.md b/docs/framework/react/guides/caching.md index 91f6e4ea9db..dcc4f7da390 100644 --- a/docs/framework/react/guides/caching.md +++ b/docs/framework/react/guides/caching.md @@ -19,7 +19,9 @@ Let's assume we are using the default `gcTime` of **5 minutes** and the default - A new instance of `useQuery({ queryKey: ['todos'], queryFn: fetchTodos })` mounts. - Since no other queries have been made with the `['todos']` query key, this query will show a hard loading state and make a network request to fetch the data. - When the network request has completed, the returned data will be cached under the `['todos']` key. + [//]: # 'StaleNote' - The hook will mark the data as stale after the configured `staleTime` (defaults to `0`, or immediately). + [//]: # 'StaleNote' - A second instance of `useQuery({ queryKey: ['todos'], queryFn: fetchTodos })` mounts elsewhere. - Since the cache already has data for the `['todos']` key from the first query, that data is immediately returned from the cache. - The new instance triggers a new network request using its query function. diff --git a/docs/framework/react/guides/important-defaults.md b/docs/framework/react/guides/important-defaults.md index 1c71b0f758f..f17e777e566 100644 --- a/docs/framework/react/guides/important-defaults.md +++ b/docs/framework/react/guides/important-defaults.md @@ -32,9 +32,13 @@ Out of the box, TanStack Query is configured with **aggressive but sane** defaul > To change this, you can alter the default `retry` and `retryDelay` options for queries to something other than `3` and the default exponential backoff function. +[//]: # 'StructuralSharing' + - Query results by default are **structurally shared to detect if data has actually changed** and if not, **the data reference remains unchanged** to better help with value stabilization with regards to useMemo and useCallback. If this concept sounds foreign, then don't worry about it! 99.9% of the time you will not need to disable this and it makes your app more performant at zero cost to you. - > Structural sharing only works with JSON-compatible values, any other value types will always be considered as changed. If you are seeing performance issues because of large responses for example, you can disable this feature with the `config.structuralSharing` flag. If you are dealing with non-JSON compatible values in your query responses and still want to detect if data has changed or not, you can provide your own custom function as `config.structuralSharing` to compute a value from the old and new responses, retaining references as required. +[//]: # 'StructuralSharing' + +> Structural sharing only works with JSON-compatible values, any other value types will always be considered as changed. If you are seeing performance issues because of large responses for example, you can disable this feature with the `config.structuralSharing` flag. If you are dealing with non-JSON compatible values in your query responses and still want to detect if data has changed or not, you can provide your own custom function as `config.structuralSharing` to compute a value from the old and new responses, retaining references as required. [//]: # 'Materials' diff --git a/docs/framework/react/guides/mutations.md b/docs/framework/react/guides/mutations.md index 6cee474450f..c7b71c46f6d 100644 --- a/docs/framework/react/guides/mutations.md +++ b/docs/framework/react/guides/mutations.md @@ -340,11 +340,14 @@ queryClient.resumePausedMutations() ``` [//]: # 'Example10' +[//]: # 'PersistOfflineIntro' ### Persisting Offline mutations If you persist offline mutations with the [persistQueryClient plugin](../plugins/persistQueryClient.md), mutations cannot be resumed when the page is reloaded unless you provide a default mutation function. +[//]: # 'PersistOfflineIntro' + This is a technical limitation. When persisting to an external storage, only the state of mutations is persisted, as functions cannot be serialized. After hydration, the component that triggers the mutation might not be mounted, so calling `resumePausedMutations` might yield an error: `No mutationFn found`. [//]: # 'Example11' @@ -385,9 +388,12 @@ export default function App() { ``` [//]: # 'Example11' +[//]: # 'OfflineExampleLink' We also have an extensive [offline example](../examples/offline) that covers both queries and mutations. +[//]: # 'OfflineExampleLink' + ## Mutation Scopes Per default, all mutations run in parallel - even if you invoke `.mutate()` of the same mutation multiple times. Mutations can be given a `scope` with an `id` to avoid that. All mutations with the same `scope.id` will run in serial, which means when they are triggered, they will start in `isPaused: true` state if there is already a mutation for that scope in progress. They will be put into a queue and will automatically resume once their time in the queue has come. diff --git a/docs/framework/react/guides/parallel-queries.md b/docs/framework/react/guides/parallel-queries.md index 0392a1cc9f2..5d078b134ea 100644 --- a/docs/framework/react/guides/parallel-queries.md +++ b/docs/framework/react/guides/parallel-queries.md @@ -35,9 +35,11 @@ function App () { If the number of queries you need to execute is changing from render to render, you cannot use manual querying since that would violate the rules of hooks. Instead, TanStack Query provides a `useQueries` hook, which you can use to dynamically execute as many queries in parallel as you'd like. [//]: # 'DynamicParallelIntro' +[//]: # 'DynamicParallelDescription' `useQueries` accepts an **options object** with a **queries key** whose value is an **array of query objects**. It returns an **array of query results**: +[//]: # 'DynamicParallelDescription' [//]: # 'Example2' ```tsx diff --git a/docs/framework/react/guides/queries.md b/docs/framework/react/guides/queries.md index de961063cbc..09787da67eb 100644 --- a/docs/framework/react/guides/queries.md +++ b/docs/framework/react/guides/queries.md @@ -7,8 +7,12 @@ title: Queries A query is a declarative dependency on an asynchronous source of data that is tied to a **unique key**. A query can be used with any Promise based method (including GET and POST methods) to fetch data from a server. If your method modifies data on the server, we recommend using [Mutations](./mutations.md) instead. +[//]: # 'SubscribeDescription' + To subscribe to a query in your components or custom hooks, call the `useQuery` hook with at least: +[//]: # 'SubscribeDescription' + - A **unique key for the query** - A function that returns a promise that: - Resolves the data, or diff --git a/docs/framework/react/guides/query-cancellation.md b/docs/framework/react/guides/query-cancellation.md index 1d34c134926..77041b1035d 100644 --- a/docs/framework/react/guides/query-cancellation.md +++ b/docs/framework/react/guides/query-cancellation.md @@ -208,4 +208,8 @@ A cancel options object supports the following properties: ## Limitations +[//]: # 'Limitations' + Cancellation does not work when working with `Suspense` hooks: `useSuspenseQuery`, `useSuspenseQueries` and `useSuspenseInfiniteQuery`. + +[//]: # 'Limitations' diff --git a/docs/framework/react/guides/query-options.md b/docs/framework/react/guides/query-options.md index 03ee5d14e13..9a33a2a2154 100644 --- a/docs/framework/react/guides/query-options.md +++ b/docs/framework/react/guides/query-options.md @@ -33,8 +33,11 @@ queryClient.setQueryData(groupOptions(42).queryKey, newGroups) For Infinite Queries, a separate [`infiniteQueryOptions`](../reference/infiniteQueryOptions.md) helper is available. +[//]: # 'SelectDescription' + You can still override some options at the component level. A very common and useful pattern is to create per-component [`select`](./render-optimizations.md#select) functions: +[//]: # 'SelectDescription' [//]: # 'Example2' ```ts diff --git a/docs/framework/react/guides/request-waterfalls.md b/docs/framework/react/guides/request-waterfalls.md index e8a3a80014a..cf983fbfa2f 100644 --- a/docs/framework/react/guides/request-waterfalls.md +++ b/docs/framework/react/guides/request-waterfalls.md @@ -11,8 +11,12 @@ The [Prefetching & Router Integration guide](./prefetching.md) builds on this an The [Server Rendering & Hydration guide](./ssr.md) teaches you how to prefetch data on the server and pass that data down to the client so you don't have to fetch it again. +[//]: # 'AdvancedSSRLink' + The [Advanced Server Rendering guide](./advanced-ssr.md) further teaches you how to apply these patterns to Server Components and Streaming Server Rendering. +[//]: # 'AdvancedSSRLink' + ## What is a Request Waterfall? A request waterfall is what happens when a request for a resource (code, css, images, data) does not start until _after_ another request for a resource has finished. @@ -67,6 +71,8 @@ With this as a basis, let's look at a few different patterns that can lead to Re When a single component first fetches one query, and then another, that's a request waterfall. This can happen when the second query is a [Dependent Query](./dependent-queries.md), that is, it depends on data from the first query when fetching: +[//]: # 'DependentExample' + ```tsx // Get the user const { data: user } = useQuery({ @@ -89,10 +95,17 @@ const { }) ``` +[//]: # 'DependentExample' + While not always feasible, for optimal performance it's better to restructure your API so you can fetch both of these in a single query. In the example above, instead of first fetching `getUserByEmail` to be able to `getProjectsByUser`, introducing a new `getProjectsByUserEmail` query would flatten the waterfall. +[//]: # 'ServerComponentsNote1' + > Another way to mitigate dependent queries without restructuring your API is to move the waterfall to the server where latency is lower. This is the idea behind Server Components which are covered in the [Advanced Server Rendering guide](./advanced-ssr.md). +[//]: # 'ServerComponentsNote1' +[//]: # 'SuspenseSerial' + Another example of serial queries is when you use React Query with Suspense: ```tsx @@ -122,14 +135,22 @@ const [usersQuery, teamsQuery, projectsQuery] = useSuspenseQueries({ }) ``` +[//]: # 'SuspenseSerial' + ### Nested Component Waterfalls +[//]: # 'NestedIntro' + Nested Component Waterfalls is when both a parent and a child component contains queries, and the parent does not render the child until its query is done. This can happen both with `useQuery` and `useSuspenseQuery`. +[//]: # 'NestedIntro' + If the child renders conditionally based on the data in the parent, or if the child relies on some part of the result being passed down as a prop from the parent to make its query, we have a _dependent_ nested component waterfall. Let's first look at an example where the child is **not** dependent on the parent. +[//]: # 'NestedExample' + ```tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ @@ -161,8 +182,12 @@ function Comments({ id }) { } ``` +[//]: # 'NestedExample' + Note that while `` takes a prop `id` from the parent, that id is already available when the `
` renders so there is no reason we could not fetch the comments at the same time as the article. In real world applications, the child might be nested far below the parent and these kinds of waterfalls are often trickier to spot and fix, but for our example, one way to flatten the waterfall would be to hoist the comments query to the parent instead: +[//]: # 'NestedHoistedExample' + ```tsx function Article({ id }) { const { data: articleData, isPending: articlePending } = useQuery({ @@ -193,12 +218,19 @@ function Article({ id }) { } ``` +[//]: # 'NestedHoistedExample' +[//]: # 'NestedHoistedOutro' + The two queries will now fetch in parallel. Note that if you are using suspense, you'd want to combine these two queries into a single `useSuspenseQueries` instead. +[//]: # 'NestedHoistedOutro' + Another way to flatten this waterfall would be to prefetch the comments in the `
` component, or prefetch both of these queries at the router level on page load or page navigation, read more about this in the [Prefetching & Router Integration guide](./prefetching.md). Next, let's look at a _Dependent Nested Component Waterfall_. +[//]: # 'DependentNestedExample' + ```tsx function Feed() { const { data, isPending } = useQuery({ @@ -233,6 +265,8 @@ function GraphFeedItem({ feedItem }) { } ``` +[//]: # 'DependentNestedExample' + The second query `getGraphDataById` is dependent on its parent in two different ways. First of all, it doesn't ever happen unless the `feedItem` is a graph, and second, it needs an `id` from the parent. ``` @@ -240,8 +274,12 @@ The second query `getGraphDataById` is dependent on its parent in two different 2. |> getGraphDataById() ``` +[//]: # 'ServerComponentsNote2' + In this example, we can't trivially flatten the waterfall by just hoisting the query to the parent, or even adding prefetching. Just like the dependent query example at the beginning of this guide, one option is to refactor our API to include the graph data in the `getFeed` query. Another more advanced solution is to leverage Server Components to move the waterfall to the server where latency is lower (read more about this in the [Advanced Server Rendering guide](./advanced-ssr.md)) but note that this can be a very big architectural change. +[//]: # 'ServerComponentsNote2' + You can have good performance even with a few query waterfalls here and there, just know they are a common performance concern and be mindful about them. An especially insidious version is when Code Splitting is involved, let's take a look at this next. ### Code Splitting @@ -323,6 +361,8 @@ In the code split case, it might actually help to hoist the `getGraphDataById` q This is very much a tradeoff however. You are now including the data fetching code for `getGraphDataById` in the same bundle as ``, so evaluate what is best for your case. Read more about how to do this in the [Prefetching & Router Integration guide](./prefetching.md). +[//]: # 'ServerComponentsNote3' + > The tradeoff between: > > - Include all data fetching code in the main bundle, even if we seldom use it @@ -330,6 +370,8 @@ This is very much a tradeoff however. You are now including the data fetching co > > is not great and has been one of the motivations for Server Components. With Server Components, it's possible to avoid both, read more about how this applies to React Query in the [Advanced Server Rendering guide](./advanced-ssr.md). +[//]: # 'ServerComponentsNote3' + ## Summary and takeaways Request Waterfalls are a very common and complex performance concern with many tradeoffs. There are many ways to accidentally introduce them into your application: diff --git a/docs/framework/solid/guides/background-fetching-indicators.md b/docs/framework/solid/guides/background-fetching-indicators.md index 56953cba06b..1aa37cfaa92 100644 --- a/docs/framework/solid/guides/background-fetching-indicators.md +++ b/docs/framework/solid/guides/background-fetching-indicators.md @@ -2,5 +2,56 @@ id: background-fetching-indicators title: Background Fetching Indicators ref: docs/framework/react/guides/background-fetching-indicators.md -replace: { 'useMutation[(]': 'useMutation(() => ' } +replace: { 'hook': 'function' } --- + +[//]: # 'Example' + +```tsx +import { Switch, Match, Show, For } from 'solid-js' + +function Todos() { + const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: fetchTodos, + })) + + return ( + + + Loading... + + + Error: {todosQuery.error.message} + + + +
Refreshing...
+
+
+ {(todo) => } +
+
+
+ ) +} +``` + +[//]: # 'Example' +[//]: # 'Example2' + +```tsx +import { useIsFetching } from '@tanstack/solid-query' + +function GlobalLoadingIndicator() { + const isFetching = useIsFetching() + + return ( + +
Queries are fetching in the background...
+
+ ) +} +``` + +[//]: # 'Example2' diff --git a/docs/framework/solid/guides/caching.md b/docs/framework/solid/guides/caching.md index de22679eb73..6f0b0ba4970 100644 --- a/docs/framework/solid/guides/caching.md +++ b/docs/framework/solid/guides/caching.md @@ -2,4 +2,14 @@ id: caching title: Caching Examples ref: docs/framework/react/guides/caching.md +replace: + { + 'useQuery[(][{] queryKey': 'useQuery(() => ({ queryKey', + 'fetchTodos [}][)]': 'fetchTodos }))', + } --- + +[//]: # 'StaleNote' + +- The data will be marked as stale after the configured `staleTime` (defaults to `0`, or immediately). + [//]: # 'StaleNote' diff --git a/docs/framework/solid/guides/default-query-function.md b/docs/framework/solid/guides/default-query-function.md index ace2813816f..9d9efa4fe0f 100644 --- a/docs/framework/solid/guides/default-query-function.md +++ b/docs/framework/solid/guides/default-query-function.md @@ -2,13 +2,52 @@ id: default-query-function title: Default Query Function ref: docs/framework/react/guides/default-query-function.md -replace: - { - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - } --- + +[//]: # 'Example' + +```tsx +// Define a default query function that will receive the query key +const defaultQueryFn = async ({ queryKey }) => { + const { data } = await axios.get( + `https://jsonplaceholder.typicode.com${queryKey[0]}`, + ) + return data +} + +// provide the default query function to your app with defaultOptions +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + queryFn: defaultQueryFn, + }, + }, +}) + +function App() { + return ( + + + + ) +} + +// All you have to do now is pass a key! +function Posts() { + const postsQuery = useQuery(() => ({ queryKey: ['/posts'] })) + + // ... +} + +// You can even leave out the queryFn and just go straight into options +function Post(props) { + const postQuery = useQuery(() => ({ + queryKey: [`/posts/${props.postId}`], + enabled: !!props.postId, + })) + + // ... +} +``` + +[//]: # 'Example' diff --git a/docs/framework/solid/guides/dependent-queries.md b/docs/framework/solid/guides/dependent-queries.md index 8ecddd7d9c1..8a646e4aa11 100644 --- a/docs/framework/solid/guides/dependent-queries.md +++ b/docs/framework/solid/guides/dependent-queries.md @@ -2,13 +2,50 @@ id: dependent-queries title: Dependent Queries ref: docs/framework/react/guides/dependent-queries.md -replace: - { - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - } --- + +[//]: # 'Example' + +```tsx +// Get the user +const userQuery = useQuery(() => ({ + queryKey: ['user', email], + queryFn: getUserByEmail, +})) + +const userId = () => userQuery.data?.id + +// Then get the user's projects +const projectsQuery = useQuery(() => ({ + queryKey: ['projects', userId()], + queryFn: getProjectsByUser, + // The query will not execute until the userId exists + enabled: !!userId(), +})) +``` + +[//]: # 'Example' +[//]: # 'Example2' + +```tsx +// Get the users ids +const usersQuery = useQuery(() => ({ + queryKey: ['users'], + queryFn: getUsersData, + select: (users) => users.map((user) => user.id), +})) + +// Then get the users messages +const usersMessages = useQueries(() => ({ + queries: usersQuery.data + ? usersQuery.data.map((id) => { + return { + queryKey: ['messages', id], + queryFn: () => getMessagesByUsers(id), + } + }) + : [], // if usersQuery.data is undefined, an empty array will be returned +})) +``` + +[//]: # 'Example2' diff --git a/docs/framework/solid/guides/disabling-queries.md b/docs/framework/solid/guides/disabling-queries.md index 8813ed376f5..ab73980147c 100644 --- a/docs/framework/solid/guides/disabling-queries.md +++ b/docs/framework/solid/guides/disabling-queries.md @@ -2,13 +2,101 @@ id: disabling-queries title: Disabling/Pausing Queries ref: docs/framework/react/guides/disabling-queries.md -replace: - { - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - } --- + +[//]: # 'Example' + +```tsx +import { Switch, Match, Show, For } from 'solid-js' + +function Todos() { + const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: fetchTodoList, + enabled: false, + })) + + return ( +
+ + + + +
    + {(todo) =>
  • {todo.title}
  • }
    +
+
+ + Error: {todosQuery.error.message} + + + Loading... + + + Not ready ... + +
+ +
{todosQuery.isFetching ? 'Fetching...' : null}
+
+ ) +} +``` + +[//]: # 'Example' +[//]: # 'Example2' + +```tsx +import { createSignal } from 'solid-js' + +function Todos() { + const [filter, setFilter] = createSignal('') + + const todosQuery = useQuery(() => ({ + queryKey: ['todos', filter()], + queryFn: () => fetchTodos(filter()), + // ⬇️ disabled as long as the filter is empty + enabled: !!filter(), + })) + + return ( +
+ {/* 🚀 applying the filter will enable and execute the query */} + + + + +
+ ) +} +``` + +[//]: # 'Example2' +[//]: # 'Example3' + +```tsx +import { createSignal } from 'solid-js' +import { skipToken, useQuery } from '@tanstack/solid-query' + +function Todos() { + const [filter, setFilter] = createSignal() + + const todosQuery = useQuery(() => ({ + queryKey: ['todos', filter()], + // ⬇️ disabled as long as the filter is undefined or empty + queryFn: filter() ? () => fetchTodos(filter()!) : skipToken, + })) + + return ( +
+ {/* 🚀 applying the filter will enable and execute the query */} + + + + +
+ ) +} +``` + +[//]: # 'Example3' diff --git a/docs/framework/solid/guides/important-defaults.md b/docs/framework/solid/guides/important-defaults.md index 1f9605fc306..1c7d7bf6b8d 100644 --- a/docs/framework/solid/guides/important-defaults.md +++ b/docs/framework/solid/guides/important-defaults.md @@ -3,3 +3,11 @@ id: important-defaults title: Important Defaults ref: docs/framework/react/guides/important-defaults.md --- + +[//]: # 'StructuralSharing' + +- Query results by default are **structurally shared to detect if data has actually changed** and if not, **the data reference remains unchanged** to better help with value stabilization. If this concept sounds foreign, then don't worry about it! 99.9% of the time you will not need to disable this and it makes your app more performant at zero cost to you. + +[//]: # 'StructuralSharing' +[//]: # 'Materials' +[//]: # 'Materials' diff --git a/docs/framework/solid/guides/infinite-queries.md b/docs/framework/solid/guides/infinite-queries.md index 00e023ece82..c6205f20d33 100644 --- a/docs/framework/solid/guides/infinite-queries.md +++ b/docs/framework/solid/guides/infinite-queries.md @@ -2,13 +2,139 @@ id: infinite-queries title: Infinite Queries ref: docs/framework/react/guides/infinite-queries.md -replace: - { - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - } --- + +[//]: # 'Example' + +```tsx +import { Switch, Match, For, Show } from 'solid-js' +import { useInfiniteQuery } from '@tanstack/solid-query' + +function Projects() { + const fetchProjects = async ({ pageParam }) => { + const res = await fetch('/api/projects?cursor=' + pageParam) + return res.json() + } + + const projectsQuery = useInfiniteQuery(() => ({ + queryKey: ['projects'], + queryFn: fetchProjects, + initialPageParam: 0, + getNextPageParam: (lastPage, pages) => lastPage.nextCursor, + })) + + return ( + + +

Loading...

+
+ +

Error: {projectsQuery.error.message}

+
+ + + {(group) => ( + {(project) =>

{project.name}

}
+ )} +
+
+ +
+ +
Fetching...
+
+
+
+ ) +} +``` + +[//]: # 'Example' +[//]: # 'Example1' + +```jsx + + projectsQuery.hasNextPage && + !projectsQuery.isFetching && + projectsQuery.fetchNextPage() + } +/> +``` + +[//]: # 'Example1' +[//]: # 'Example3' + +```tsx +useInfiniteQuery(() => ({ + queryKey: ['projects'], + queryFn: fetchProjects, + initialPageParam: 0, + getNextPageParam: (lastPage, pages) => lastPage.nextCursor, + getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, +})) +``` + +[//]: # 'Example3' +[//]: # 'Example4' + +```tsx +useInfiniteQuery(() => ({ + queryKey: ['projects'], + queryFn: fetchProjects, + select: (data) => ({ + pages: [...data.pages].reverse(), + pageParams: [...data.pageParams].reverse(), + }), +})) +``` + +[//]: # 'Example4' +[//]: # 'Example8' + +```tsx +useInfiniteQuery(() => ({ + queryKey: ['projects'], + queryFn: fetchProjects, + initialPageParam: 0, + getNextPageParam: (lastPage, pages) => lastPage.nextCursor, + getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, + maxPages: 3, +})) +``` + +[//]: # 'Example8' +[//]: # 'Example9' + +```tsx +return useInfiniteQuery(() => ({ + queryKey: ['projects'], + queryFn: fetchProjects, + initialPageParam: 0, + getNextPageParam: (lastPage, allPages, lastPageParam) => { + if (lastPage.length === 0) { + return undefined + } + return lastPageParam + 1 + }, + getPreviousPageParam: (firstPage, allPages, firstPageParam) => { + if (firstPageParam <= 1) { + return undefined + } + return firstPageParam - 1 + }, +})) +``` + +[//]: # 'Example9' diff --git a/docs/framework/solid/guides/initial-query-data.md b/docs/framework/solid/guides/initial-query-data.md index db37819777c..4aeb0fd281d 100644 --- a/docs/framework/solid/guides/initial-query-data.md +++ b/docs/framework/solid/guides/initial-query-data.md @@ -12,3 +12,116 @@ replace: 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', } --- + +[//]: # 'Example' + +```tsx +const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: () => fetch('/todos'), + initialData: initialTodos, +})) +``` + +[//]: # 'Example' +[//]: # 'Example2' + +```tsx +// Will show initialTodos immediately, but also immediately refetch todos after mount +const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: () => fetch('/todos'), + initialData: initialTodos, +})) +``` + +[//]: # 'Example2' +[//]: # 'Example3' + +```tsx +// Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms +const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: () => fetch('/todos'), + initialData: initialTodos, + staleTime: 1000, +})) +``` + +[//]: # 'Example3' +[//]: # 'Example4' + +```tsx +// Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms +const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: () => fetch('/todos'), + initialData: initialTodos, + staleTime: 60 * 1000, // 1 minute + // This could be 10 seconds ago or 10 minutes ago + initialDataUpdatedAt: initialTodosUpdatedTimestamp, // eg. 1608412420052 +})) +``` + +[//]: # 'Example4' +[//]: # 'Example5' + +```tsx +const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: () => fetch('/todos'), + initialData: () => getExpensiveTodos(), +})) +``` + +[//]: # 'Example5' +[//]: # 'Example6' + +```tsx +const todoQuery = useQuery(() => ({ + queryKey: ['todo', todoId], + queryFn: () => fetch('/todos'), + initialData: () => { + // Use a todo from the 'todos' query as the initial data for this todo query + return queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId) + }, +})) +``` + +[//]: # 'Example6' +[//]: # 'Example7' + +```tsx +const todoQuery = useQuery(() => ({ + queryKey: ['todos', todoId], + queryFn: () => fetch(`/todos/${todoId}`), + initialData: () => + queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId), + initialDataUpdatedAt: () => + queryClient.getQueryState(['todos'])?.dataUpdatedAt, +})) +``` + +[//]: # 'Example7' +[//]: # 'Example8' + +```tsx +const todoQuery = useQuery(() => ({ + queryKey: ['todo', todoId], + queryFn: () => fetch(`/todos/${todoId}`), + initialData: () => { + // Get the query state + const state = queryClient.getQueryState(['todos']) + + // If the query exists and has data that is no older than 10 seconds... + if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) { + // return the individual todo + return state.data.find((d) => d.id === todoId) + } + + // Otherwise, return undefined and let it fetch from a hard loading state! + }, +})) +``` + +[//]: # 'Example8' diff --git a/docs/framework/solid/guides/invalidations-from-mutations.md b/docs/framework/solid/guides/invalidations-from-mutations.md index 5246a5a1192..ae327870335 100644 --- a/docs/framework/solid/guides/invalidations-from-mutations.md +++ b/docs/framework/solid/guides/invalidations-from-mutations.md @@ -11,5 +11,6 @@ replace: 'useQuery[(]': 'useQuery(() => ', 'useQueries[(]': 'useQueries(() => ', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', + 'hook': 'function', } --- diff --git a/docs/framework/solid/guides/mutations.md b/docs/framework/solid/guides/mutations.md index 6eca902d7f1..8b6c22fbcad 100644 --- a/docs/framework/solid/guides/mutations.md +++ b/docs/framework/solid/guides/mutations.md @@ -2,14 +2,299 @@ id: mutations title: Mutations ref: docs/framework/react/guides/mutations.md -replace: - { - 'React': 'Solid', - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - } +replace: { 'hook': 'function' } --- + +[//]: # 'Example' + +```tsx +import { Switch, Match, Show } from 'solid-js' + +function App() { + const mutation = useMutation(() => ({ + mutationFn: (newTodo) => { + return axios.post('/todos', newTodo) + }, + })) + + return ( +
+ + Adding todo... + + +
An error occurred: {mutation.error.message}
+
+ + +
Todo added!
+
+ + +
+
+
+ ) +} +``` + +[//]: # 'Example' +[//]: # 'Info1' +[//]: # 'Info1' +[//]: # 'Example2' + +```tsx +const CreateTodo = () => { + const mutation = useMutation(() => ({ + mutationFn: (formData) => { + return fetch('/api', formData) + }, + })) + const onSubmit = (event) => { + event.preventDefault() + mutation.mutate(new FormData(event.target)) + } + + return
...
+} +``` + +[//]: # 'Example2' +[//]: # 'Example3' + +```tsx +import { createSignal, Show } from 'solid-js' + +const CreateTodo = () => { + const [title, setTitle] = createSignal('') + const mutation = useMutation(() => ({ mutationFn: createTodo })) + + const onCreateTodo = (e) => { + e.preventDefault() + mutation.mutate({ title: title() }) + } + + return ( +
+ +
mutation.reset()}>{mutation.error}
+
+ setTitle(e.currentTarget.value)} + /> +
+ +
+ ) +} +``` + +[//]: # 'Example3' +[//]: # 'Example4' + +```tsx +useMutation(() => ({ + mutationFn: addTodo, + onMutate: (variables, context) => { + // A mutation is about to happen! + + // Optionally return a result containing data to use when for example rolling back + return { id: 1 } + }, + onError: (error, variables, onMutateResult, context) => { + // An error happened! + console.log(`rolling back optimistic update with id ${onMutateResult.id}`) + }, + onSuccess: (data, variables, onMutateResult, context) => { + // Boom baby! + }, + onSettled: (data, error, variables, onMutateResult, context) => { + // Error or success... doesn't matter! + }, +})) +``` + +[//]: # 'Example4' +[//]: # 'Example5' + +```tsx +useMutation(() => ({ + mutationFn: addTodo, + onSuccess: async () => { + console.log("I'm first!") + }, + onSettled: async () => { + console.log("I'm second!") + }, +})) +``` + +[//]: # 'Example5' +[//]: # 'Example6' + +```tsx +useMutation(() => ({ + mutationFn: addTodo, + onSuccess: (data, variables, onMutateResult, context) => { + // I will fire first + }, + onError: (error, variables, onMutateResult, context) => { + // I will fire first + }, + onSettled: (data, error, variables, onMutateResult, context) => { + // I will fire first + }, +})) + +mutate(todo, { + onSuccess: (data, variables, onMutateResult, context) => { + // I will fire second! + }, + onError: (error, variables, onMutateResult, context) => { + // I will fire second! + }, + onSettled: (data, error, variables, onMutateResult, context) => { + // I will fire second! + }, +}) +``` + +[//]: # 'Example6' +[//]: # 'Example7' + +```tsx +useMutation(() => ({ + mutationFn: addTodo, + onSuccess: (data, variables, onMutateResult, context) => { + // Will be called 3 times + }, +})) + +const todos = ['Todo 1', 'Todo 2', 'Todo 3'] +todos.forEach((todo) => { + mutate(todo, { + onSuccess: (data, variables, onMutateResult, context) => { + // Will execute only once, for the last mutation (Todo 3), + // regardless which mutation resolves first + }, + }) +}) +``` + +[//]: # 'Example7' +[//]: # 'Example8' + +```tsx +const mutation = useMutation(() => ({ mutationFn: addTodo })) + +try { + const todo = await mutation.mutateAsync(todo) + console.log(todo) +} catch (error) { + console.error(error) +} finally { + console.log('done') +} +``` + +[//]: # 'Example8' +[//]: # 'Example9' + +```tsx +const mutation = useMutation(() => ({ + mutationFn: addTodo, + retry: 3, +})) +``` + +[//]: # 'Example9' +[//]: # 'Example10' + +```tsx +const queryClient = new QueryClient() + +// Define the "addTodo" mutation +queryClient.setMutationDefaults(['addTodo'], { + mutationFn: addTodo, + onMutate: async (variables, context) => { + // Cancel current queries for the todos list + await context.client.cancelQueries({ queryKey: ['todos'] }) + + // Create optimistic todo + const optimisticTodo = { id: uuid(), title: variables.title } + + // Add optimistic todo to todos list + context.client.setQueryData(['todos'], (old) => [...old, optimisticTodo]) + + // Return a result with the optimistic todo + return { optimisticTodo } + }, + onSuccess: (result, variables, onMutateResult, context) => { + // Replace optimistic todo in the todos list with the result + context.client.setQueryData(['todos'], (old) => + old.map((todo) => + todo.id === onMutateResult.optimisticTodo.id ? result : todo, + ), + ) + }, + onError: (error, variables, onMutateResult, context) => { + // Remove optimistic todo from the todos list + context.client.setQueryData(['todos'], (old) => + old.filter((todo) => todo.id !== onMutateResult.optimisticTodo.id), + ) + }, + retry: 3, +}) + +// Start mutation in some component: +const mutation = useMutation(() => ({ mutationKey: ['addTodo'] })) +mutation.mutate({ title: 'title' }) + +// If the mutation has been paused because the device is for example offline, +// Then the paused mutation can be dehydrated when the application quits: +const state = dehydrate(queryClient) + +// The mutation can then be hydrated again when the application is started: +hydrate(queryClient, state) + +// Resume the paused mutations: +queryClient.resumePausedMutations() +``` + +[//]: # 'Example10' +[//]: # 'PersistOfflineIntro' + +### Persisting Offline mutations + +If you persist offline mutations with the `persistQueryClient` plugin, mutations cannot be resumed when the page is reloaded unless you provide a default mutation function. + +[//]: # 'PersistOfflineIntro' +[//]: # 'OfflineExampleLink' +[//]: # 'OfflineExampleLink' +[//]: # 'ExampleScopes' + +```tsx +const mutation = useMutation(() => ({ + mutationFn: addTodo, + scope: { + id: 'todo', + }, +})) +``` + +[//]: # 'ExampleScopes' +[//]: # 'Materials' + +## Further reading + +For more information about mutations, have a look at [TkDodo's article on Mastering Mutations in TanStack Query](https://tkdodo.eu/blog/mastering-mutations-in-react-query). + +[//]: # 'Materials' diff --git a/docs/framework/solid/guides/optimistic-updates.md b/docs/framework/solid/guides/optimistic-updates.md index f45c88e3b48..9b8a28b8d3d 100644 --- a/docs/framework/solid/guides/optimistic-updates.md +++ b/docs/framework/solid/guides/optimistic-updates.md @@ -2,14 +2,145 @@ id: optimistic-updates title: Optimistic Updates ref: docs/framework/react/guides/optimistic-updates.md -replace: - { - 'React': 'Solid', - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - } +replace: { 'React Query': 'Solid Query', 'hook': 'function' } --- + +[//]: # 'ExampleUI1' + +```tsx +const addTodoMutation = useMutation(() => ({ + mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), + // make sure to _return_ the Promise from the query invalidation + // so that the mutation stays in `pending` state until the refetch is finished + onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), +})) +``` + +[//]: # 'ExampleUI1' +[//]: # 'ExampleUI2' + +```tsx +
    + {(todo) =>
  • {todo.text}
  • }
    + +
  • {addTodoMutation.variables}
  • +
    +
+``` + +[//]: # 'ExampleUI2' +[//]: # 'ExampleUI3' + +```tsx + +
  • + {addTodoMutation.variables} + +
  • +
    +``` + +[//]: # 'ExampleUI3' +[//]: # 'ExampleUI4' + +```tsx +// somewhere in your app +const mutation = useMutation(() => ({ + mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), + onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), + mutationKey: ['addTodo'], +})) + +// access variables somewhere else +const variables = useMutationState(() => ({ + filters: { mutationKey: ['addTodo'], status: 'pending' }, + select: (mutation) => mutation.state.variables, +})) +``` + +[//]: # 'ExampleUI4' +[//]: # 'Example' + +```tsx +const queryClient = useQueryClient() + +useMutation(() => ({ + mutationFn: updateTodo, + // When mutate is called: + onMutate: async (newTodo, context) => { + // Cancel any outgoing refetches + // (so they don't overwrite our optimistic update) + await context.client.cancelQueries({ queryKey: ['todos'] }) + + // Snapshot the previous value + const previousTodos = context.client.getQueryData(['todos']) + + // Optimistically update to the new value + context.client.setQueryData(['todos'], (old) => [...old, newTodo]) + + // Return a result with the snapshotted value + return { previousTodos } + }, + // If the mutation fails, + // use the result returned from onMutate to roll back + onError: (err, newTodo, onMutateResult, context) => { + context.client.setQueryData(['todos'], onMutateResult.previousTodos) + }, + // Always refetch after error or success: + onSettled: (data, error, variables, onMutateResult, context) => + context.client.invalidateQueries({ queryKey: ['todos'] }), +})) +``` + +[//]: # 'Example' +[//]: # 'Example2' + +```tsx +useMutation(() => ({ + mutationFn: updateTodo, + // When mutate is called: + onMutate: async (newTodo, context) => { + // Cancel any outgoing refetches + // (so they don't overwrite our optimistic update) + await context.client.cancelQueries({ queryKey: ['todos', newTodo.id] }) + + // Snapshot the previous value + const previousTodo = context.client.getQueryData(['todos', newTodo.id]) + + // Optimistically update to the new value + context.client.setQueryData(['todos', newTodo.id], newTodo) + + // Return a result with the previous and new todo + return { previousTodo, newTodo } + }, + // If the mutation fails, use the result we returned above + onError: (err, newTodo, onMutateResult, context) => { + context.client.setQueryData( + ['todos', onMutateResult.newTodo.id], + onMutateResult.previousTodo, + ) + }, + // Always refetch after error or success: + onSettled: (newTodo, error, variables, onMutateResult, context) => + context.client.invalidateQueries({ queryKey: ['todos', newTodo.id] }), +})) +``` + +[//]: # 'Example2' +[//]: # 'Example3' + +```tsx +useMutation(() => ({ + mutationFn: updateTodo, + // ... + onSettled: async (newTodo, error, variables, onMutateResult, context) => { + if (error) { + // do something + } + }, +})) +``` + +[//]: # 'Example3' diff --git a/docs/framework/solid/guides/paginated-queries.md b/docs/framework/solid/guides/paginated-queries.md index 058b1d5a456..5a15013e1e7 100644 --- a/docs/framework/solid/guides/paginated-queries.md +++ b/docs/framework/solid/guides/paginated-queries.md @@ -2,13 +2,80 @@ id: paginated-queries title: Paginated / Lagged Queries ref: docs/framework/react/guides/paginated-queries.md -replace: - { - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - } +replace: { 'hook': 'function' } --- + +[//]: # 'Example' + +```tsx +const projectsQuery = useQuery(() => ({ + queryKey: ['projects', page()], + queryFn: () => fetchProjects(page()), +})) +``` + +[//]: # 'Example' +[//]: # 'Example2' + +```tsx +import { createSignal, Switch, Match, Show, For } from 'solid-js' +import { keepPreviousData, useQuery } from '@tanstack/solid-query' + +function Todos() { + const [page, setPage] = createSignal(0) + + const fetchProjects = (page = 0) => + fetch('/api/projects?page=' + page).then((res) => res.json()) + + const projectsQuery = useQuery(() => ({ + queryKey: ['projects', page()], + queryFn: () => fetchProjects(page()), + placeholderData: keepPreviousData, + })) + + return ( +
    + + +
    Loading...
    +
    + +
    Error: {projectsQuery.error.message}
    +
    + +
    + + {(project) =>

    {project.name}

    } +
    +
    +
    +
    + Current Page: {page() + 1} + + + + Loading... + +
    + ) +} +``` + +[//]: # 'Example2' diff --git a/docs/framework/solid/guides/parallel-queries.md b/docs/framework/solid/guides/parallel-queries.md index 2729ca37c73..e7481a8f6b4 100644 --- a/docs/framework/solid/guides/parallel-queries.md +++ b/docs/framework/solid/guides/parallel-queries.md @@ -10,5 +10,48 @@ replace: 'useQuery[(]': 'useQuery(() => ', 'useQueries[(]': 'useQueries(() => ', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', + 'hooks': 'functions', } --- + +[//]: # 'Example' + +```tsx +function App () { + // The following queries will execute in parallel + const usersQuery = useQuery(() => ({ queryKey: ['users'], queryFn: fetchUsers })) + const teamsQuery = useQuery(() => ({ queryKey: ['teams'], queryFn: fetchTeams })) + const projectsQuery = useQuery(() => ({ queryKey: ['projects'], queryFn: fetchProjects })) + ... +} +``` + +[//]: # 'Example' +[//]: # 'Info' +[//]: # 'Info' +[//]: # 'DynamicParallelIntro' + +If the number of queries you need to execute changes, you cannot use manual querying since that would break reactivity. Instead, TanStack Query provides a `useQueries` function, which you can use to dynamically execute as many queries in parallel as you'd like. + +[//]: # 'DynamicParallelIntro' +[//]: # 'DynamicParallelDescription' + +`useQueries` accepts an **accessor that returns an options object** with a **queries key** whose value is an **array of query objects**. It returns an **array of query results**: + +[//]: # 'DynamicParallelDescription' +[//]: # 'Example2' + +```tsx +function App(props) { + const userQueries = useQueries(() => ({ + queries: props.users.map((user) => { + return { + queryKey: ['user', user.id], + queryFn: () => fetchUserById(user.id), + } + }), + })) +} +``` + +[//]: # 'Example2' diff --git a/docs/framework/solid/guides/placeholder-query-data.md b/docs/framework/solid/guides/placeholder-query-data.md index 94b137a2485..7249f410909 100644 --- a/docs/framework/solid/guides/placeholder-query-data.md +++ b/docs/framework/solid/guides/placeholder-query-data.md @@ -2,15 +2,67 @@ id: placeholder-query-data title: Placeholder Query Data ref: docs/framework/react/guides/placeholder-query-data.md -replace: - { - 'React': 'Solid', - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - 'useMemo': 'createMemo', - } --- + +[//]: # 'ExampleValue' + +```tsx +function Todos() { + const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: () => fetch('/todos'), + placeholderData: placeholderTodos, + })) +} +``` + +[//]: # 'ExampleValue' +[//]: # 'Memoization' + +### Placeholder Data Memoization + +If the process for accessing a query's placeholder data is intensive or just not something you want to perform on every render, you can memoize the value: + +```tsx +function Todos() { + const placeholderData = createMemo(() => generateFakeTodos()) + const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: () => fetch('/todos'), + placeholderData: placeholderData(), + })) +} +``` + +[//]: # 'Memoization' +[//]: # 'ExampleFunction' + +```tsx +const todosQuery = useQuery(() => ({ + queryKey: ['todos', id], + queryFn: () => fetch(`/todos/${id}`), + placeholderData: (previousData, previousQuery) => previousData, +})) +``` + +[//]: # 'ExampleFunction' +[//]: # 'ExampleCache' + +```tsx +function BlogPost(props) { + const queryClient = useQueryClient() + const blogPostQuery = useQuery(() => ({ + queryKey: ['blogPost', props.blogPostId], + queryFn: () => fetch(`/blogPosts/${props.blogPostId}`), + placeholderData: () => { + // Use the smaller/preview version of the blogPost from the 'blogPosts' + // query as the placeholder data for this blogPost query + return queryClient + .getQueryData(['blogPosts']) + ?.find((d) => d.id === props.blogPostId) + }, + })) +} +``` + +[//]: # 'ExampleCache' diff --git a/docs/framework/solid/guides/prefetching.md b/docs/framework/solid/guides/prefetching.md index 9352d1a24f4..8c7a7720020 100644 --- a/docs/framework/solid/guides/prefetching.md +++ b/docs/framework/solid/guides/prefetching.md @@ -1,16 +1,257 @@ --- id: prefetching -title: Prefetching +title: Prefetching & Router Integration ref: docs/framework/react/guides/prefetching.md -replace: - { - 'React.lazy': 'Solid.lazy', - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - '/docs/framework/react/examples/basic-react-query-file-based': '/docs/framework/solid/examples/basic-solid-query-file-based', - } --- + +[//]: # 'ExampleComponent' + +```tsx +import { Switch, Match } from 'solid-js' + +function Article(props) { + const articleQuery = useQuery(() => ({ + queryKey: ['article', props.id], + queryFn: getArticleById, + })) + + return ( + + + Loading article... + + + + + + + + ) +} + +function Comments(props) { + const commentsQuery = useQuery(() => ({ + queryKey: ['article-comments', props.id], + queryFn: getArticleCommentsById, + })) + + ... +} +``` + +[//]: # 'ExampleComponent' +[//]: # 'ExampleParentComponent' + +```tsx +import { Switch, Match } from 'solid-js' + +function Article(props) { + const articleQuery = useQuery(() => ({ + queryKey: ['article', props.id], + queryFn: getArticleById, + })) + + // Prefetch + useQuery(() => ({ + queryKey: ['article-comments', props.id], + queryFn: getArticleCommentsById, + // Optional optimization to avoid rerenders when this query changes: + notifyOnChangeProps: [], + })) + + return ( + + + Loading article... + + + + + + + + ) +} + +function Comments(props) { + const commentsQuery = useQuery(() => ({ + queryKey: ['article-comments', props.id], + queryFn: getArticleCommentsById, + })) + + ... +} +``` + +[//]: # 'ExampleParentComponent' +[//]: # 'Suspense' + +Another way is to prefetch inside of the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use `queryClient.prefetchQuery`: + +```tsx +const queryClient = useQueryClient() +const articleQuery = useQuery(() => ({ + queryKey: ['article', id], + queryFn: (...args) => { + queryClient.prefetchQuery({ + queryKey: ['article-comments', id], + queryFn: getArticleCommentsById, + }) + + return getArticleById(...args) + }, +})) +``` + +Prefetching in an effect also works: + +```tsx +import { createEffect } from 'solid-js' + +const queryClient = useQueryClient() + +createEffect(() => { + queryClient.prefetchQuery({ + queryKey: ['article-comments', id], + queryFn: getArticleCommentsById, + }) +}) +``` + +To recap, if you want to prefetch a query during the component lifecycle, there are a few different ways to do it, pick the one that suits your situation best: + +- Use `useQuery` and ignore the result +- Prefetch inside the query function +- Prefetch in an effect + +Let's look at a slightly more advanced case next. + +[//]: # 'Suspense' +[//]: # 'ExampleConditionally1' + +```tsx +import { lazy, Switch, Match, For } from 'solid-js' + +// This lazy loads the GraphFeedItem component, meaning +// it wont start loading until something renders it +const GraphFeedItem = lazy(() => import('./GraphFeedItem')) + +function Feed() { + const feedQuery = useQuery(() => ({ + queryKey: ['feed'], + queryFn: getFeed, + })) + + return ( + + + Loading feed... + + + + {(feedItem) => { + if (feedItem.type === 'GRAPH') { + return + } + return + }} + + + + ) +} + +// GraphFeedItem.tsx +function GraphFeedItem(props) { + const graphQuery = useQuery(() => ({ + queryKey: ['graph', props.feedItem.id], + queryFn: getGraphDataById, + })) + + ... +} +``` + +[//]: # 'ExampleConditionally1' +[//]: # 'ExampleConditionally2' + +```tsx +function Feed() { + const queryClient = useQueryClient() + const feedQuery = useQuery(() => ({ + queryKey: ['feed'], + queryFn: async (...args) => { + const feed = await getFeed(...args) + + for (const feedItem of feed) { + if (feedItem.type === 'GRAPH') { + queryClient.prefetchQuery({ + queryKey: ['graph', feedItem.id], + queryFn: getGraphDataById, + }) + } + } + + return feed + } + })) + + ... +} +``` + +[//]: # 'ExampleConditionally2' +[//]: # 'Router' + +## Router Integration + +Because data fetching in the component tree itself can easily lead to request waterfalls and the different fixes for that can be cumbersome as they accumulate throughout the application, an attractive way to do prefetching is integrating it at the router level. + +In this approach, you explicitly declare for each _route_ what data is going to be needed for that component tree, ahead of time. Because Server Rendering has traditionally needed all data to be loaded before rendering starts, this has been the dominating approach for SSR'd apps for a long time. This is still a common approach and you can read more about it in the [Server Rendering & Hydration guide](./ssr.md). + +For now, let's focus on the client side case and look at an example of how you can make this work with [TanStack Router](https://tanstack.com/router). These examples leave out a lot of setup and boilerplate to stay concise, you can check out a [full Solid Query example](https://tanstack.com/router/latest/docs/framework/solid/examples/basic-solid-query-file-based) over in the [TanStack Router docs](https://tanstack.com/router/latest/docs). + +When integrating at the router level, you can choose to either _block_ rendering of that route until all data is present, or you can start a prefetch but not await the result. That way, you can start rendering the route as soon as possible. You can also mix these two approaches and await some critical data, but start rendering before all the secondary data has finished loading. In this example, we'll configure an `/article` route to not render until the article data has finished loading, as well as start prefetching comments as soon as possible, but not block rendering the route if comments haven't finished loading yet. + +```tsx +const queryClient = new QueryClient() +const routerContext = new RouterContext() +const rootRoute = routerContext.createRootRoute({ + component: () => { ... } +}) + +const articleRoute = new Route({ + getParentRoute: () => rootRoute, + path: 'article', + beforeLoad: () => { + return { + articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle }, + commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments }, + } + }, + loader: async ({ + context: { queryClient }, + routeContext: { articleQueryOptions, commentsQueryOptions }, + }) => { + // Fetch comments asap, but don't block + queryClient.prefetchQuery(commentsQueryOptions) + + // Don't render the route at all until article has been fetched + await queryClient.prefetchQuery(articleQueryOptions) + }, + component: ({ useRouteContext }) => { + const { articleQueryOptions, commentsQueryOptions } = useRouteContext() + const articleQuery = useQuery(() => articleQueryOptions) + const commentsQuery = useQuery(() => commentsQueryOptions) + + return ( + ... + ) + }, + errorComponent: () => 'Oh crap!', +}) +``` + +Integration with other routers is also possible. + +[//]: # 'Router' diff --git a/docs/framework/solid/guides/queries.md b/docs/framework/solid/guides/queries.md index 06371872478..05cb39a362e 100644 --- a/docs/framework/solid/guides/queries.md +++ b/docs/framework/solid/guides/queries.md @@ -2,12 +2,96 @@ id: queries title: Queries ref: docs/framework/react/guides/queries.md -replace: - { - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - } --- + +[//]: # 'SubscribeDescription' + +To subscribe to a query in your components, call the `useQuery` function with at least: +[//]: # 'SubscribeDescription' + +[//]: # 'Example' + +```tsx +import { useQuery } from '@tanstack/solid-query' + +function App() { + const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: fetchTodoList, + })) +} +``` + +[//]: # 'Example' +[//]: # 'Example2' + +```tsx +const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: fetchTodoList, +})) +``` + +[//]: # 'Example2' +[//]: # 'Example3' + +```tsx +import { Switch, Match, For } from 'solid-js' + +function Todos() { + const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: fetchTodoList, + })) + + return ( + + + Loading... + + + Error: {todosQuery.error.message} + + +
      + {(todo) =>
    • {todo.title}
    • }
      +
    +
    +
    + ) +} +``` + +[//]: # 'Example3' +[//]: # 'Example4' + +```tsx +import { Switch, Match, For } from 'solid-js' + +function Todos() { + const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: fetchTodoList, + })) + + return ( + + + Loading... + + + Error: {todosQuery.error.message} + + +
      + {(todo) =>
    • {todo.title}
    • }
      +
    +
    +
    + ) +} +``` + +[//]: # 'Example4' +[//]: # 'Materials' +[//]: # 'Materials' diff --git a/docs/framework/solid/guides/query-cancellation.md b/docs/framework/solid/guides/query-cancellation.md index 7278c681e79..f9284813c59 100644 --- a/docs/framework/solid/guides/query-cancellation.md +++ b/docs/framework/solid/guides/query-cancellation.md @@ -13,22 +13,153 @@ replace: } --- +[//]: # 'Example' + +```tsx +const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: async ({ signal }) => { + const todosResponse = await fetch('/todos', { + // Pass the signal to one fetch + signal, + }) + const todos = await todosResponse.json() + + const todoDetails = todos.map(async ({ details }) => { + const response = await fetch(details, { + // Or pass it to several + signal, + }) + return response.json() + }) + + return Promise.all(todoDetails) + }, +})) +``` + +[//]: # 'Example' +[//]: # 'Example2' + +```tsx +import axios from 'axios' + +const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: ({ signal }) => + axios.get('/todos', { + // Pass the signal to `axios` + signal, + }), +})) +``` + +[//]: # 'Example2' +[//]: # 'Example3' + +```tsx +import axios from 'axios' + +const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: ({ signal }) => { + // Create a new CancelToken source for this request + const CancelToken = axios.CancelToken + const source = CancelToken.source() + + const promise = axios.get('/todos', { + // Pass the source token to your request + cancelToken: source.token, + }) + + // Cancel the request if TanStack Query signals to abort + signal?.addEventListener('abort', () => { + source.cancel('Query was cancelled by TanStack Query') + }) + + return promise + }, +})) +``` + +[//]: # 'Example3' +[//]: # 'Example4' + +```tsx +const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: ({ signal }) => { + return new Promise((resolve, reject) => { + var oReq = new XMLHttpRequest() + oReq.addEventListener('load', () => { + resolve(JSON.parse(oReq.responseText)) + }) + signal?.addEventListener('abort', () => { + oReq.abort() + reject() + }) + oReq.open('GET', '/todos') + oReq.send() + }) + }, +})) +``` + +[//]: # 'Example4' +[//]: # 'Example5' + +```tsx +const client = new GraphQLClient(endpoint) + +const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: ({ signal }) => { + client.request({ document: query, signal }) + }, +})) +``` + +[//]: # 'Example5' +[//]: # 'Example6' + +```tsx +const todosQuery = useQuery(() => ({ + queryKey: ['todos'], + queryFn: ({ signal }) => { + const client = new GraphQLClient(endpoint, { + signal, + }) + return client.request(query, variables) + }, +})) +``` + +[//]: # 'Example6' [//]: # 'Example7' -```ts -const query = useQuery({ +```tsx +const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: async ({ signal }) => { const resp = await fetch('/todos', { signal }) return resp.json() }, -}) +})) const queryClient = useQueryClient() -function onButtonClick() { - queryClient.cancelQueries({ queryKey: ['todos'] }) -} +return ( + +) ``` [//]: # 'Example7' +[//]: # 'Limitations' +[//]: # 'Limitations' diff --git a/docs/framework/solid/guides/query-functions.md b/docs/framework/solid/guides/query-functions.md index 463c55095af..f612f0a942b 100644 --- a/docs/framework/solid/guides/query-functions.md +++ b/docs/framework/solid/guides/query-functions.md @@ -2,13 +2,80 @@ id: query-functions title: Query Functions ref: docs/framework/react/guides/query-functions.md -replace: - { - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - } --- + +[//]: # 'Example' + +```tsx +useQuery(() => ({ queryKey: ['todos'], queryFn: fetchAllTodos })) +useQuery(() => ({ + queryKey: ['todos', todoId], + queryFn: () => fetchTodoById(todoId), +})) +useQuery(() => ({ + queryKey: ['todos', todoId], + queryFn: async () => { + const data = await fetchTodoById(todoId) + return data + }, +})) +useQuery(() => ({ + queryKey: ['todos', todoId], + queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), +})) +``` + +[//]: # 'Example' +[//]: # 'Example2' + +```tsx +const todosQuery = useQuery(() => ({ + queryKey: ['todos', todoId], + queryFn: async () => { + if (somethingGoesWrong) { + throw new Error('Oh no!') + } + if (somethingElseGoesWrong) { + return Promise.reject(new Error('Oh no!')) + } + + return data + }, +})) +``` + +[//]: # 'Example2' +[//]: # 'Example3' + +```tsx +useQuery(() => ({ + queryKey: ['todos', todoId], + queryFn: async () => { + const response = await fetch('/todos/' + todoId) + if (!response.ok) { + throw new Error('Network response was not ok') + } + return response.json() + }, +})) +``` + +[//]: # 'Example3' +[//]: # 'Example4' + +```tsx +function Todos(props) { + const todosQuery = useQuery(() => ({ + queryKey: ['todos', { status: props.status, page: props.page }], + queryFn: fetchTodoList, + })) +} + +// Access the key, status and page variables in your query function! +function fetchTodoList({ queryKey }) { + const [_key, { status, page }] = queryKey + return new Promise() +} +``` + +[//]: # 'Example4' diff --git a/docs/framework/solid/guides/query-invalidation.md b/docs/framework/solid/guides/query-invalidation.md index 14f394d89cc..a6c7e1e9024 100644 --- a/docs/framework/solid/guides/query-invalidation.md +++ b/docs/framework/solid/guides/query-invalidation.md @@ -10,5 +10,6 @@ replace: 'useQuery[(]': 'useQuery(() => ', 'useQueries[(]': 'useQueries(() => ', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', + 'hooks': 'functions', } --- diff --git a/docs/framework/solid/guides/query-keys.md b/docs/framework/solid/guides/query-keys.md index b9bda076525..6bf37900f4c 100644 --- a/docs/framework/solid/guides/query-keys.md +++ b/docs/framework/solid/guides/query-keys.md @@ -10,5 +10,62 @@ replace: 'useQuery[(]': 'useQuery(() => ', 'useQueries[(]': 'useQueries(() => ', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', + 'React Query Keys': 'TanStack Query Keys', } --- + +[//]: # 'Example' + +```tsx +// A list of todos +useQuery(() => ({ queryKey: ['todos'], ... })) + +// Something else, whatever! +useQuery(() => ({ queryKey: ['something', 'special'], ... })) +``` + +[//]: # 'Example' +[//]: # 'Example2' + +```tsx +// An individual todo +useQuery(() => ({ queryKey: ['todo', 5], ... })) + +// An individual todo in a "preview" format +useQuery(() => ({ queryKey: ['todo', 5, { preview: true }], ...})) + +// A list of todos that are "done" +useQuery(() => ({ queryKey: ['todos', { type: 'done' }], ... })) +``` + +[//]: # 'Example2' +[//]: # 'Example3' + +```tsx +useQuery(() => ({ queryKey: ['todos', { status, page }], ... })) +useQuery(() => ({ queryKey: ['todos', { page, status }], ...})) +useQuery(() => ({ queryKey: ['todos', { page, status, other: undefined }], ... })) +``` + +[//]: # 'Example3' +[//]: # 'Example4' + +```tsx +useQuery(() => ({ queryKey: ['todos', status, page], ... })) +useQuery(() => ({ queryKey: ['todos', page, status], ...})) +useQuery(() => ({ queryKey: ['todos', undefined, page, status], ...})) +``` + +[//]: # 'Example4' +[//]: # 'Example5' + +```tsx +function Todos(props) { + const todosQuery = useQuery(() => ({ + queryKey: ['todos', props.todoId], + queryFn: () => fetchTodoById(props.todoId), + })) +} +``` + +[//]: # 'Example5' diff --git a/docs/framework/solid/guides/query-options.md b/docs/framework/solid/guides/query-options.md index d6ba80e0c95..d5a625962b9 100644 --- a/docs/framework/solid/guides/query-options.md +++ b/docs/framework/solid/guides/query-options.md @@ -12,3 +12,45 @@ replace: 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', } --- + +[//]: # 'Example1' + +```ts +import { queryOptions } from '@tanstack/solid-query' + +function groupOptions(id: number) { + return queryOptions({ + queryKey: ['groups', id], + queryFn: () => fetchGroups(id), + staleTime: 5 * 1000, + }) +} + +// usage: + +useQuery(() => groupOptions(1)) +useQueries(() => ({ + queries: [groupOptions(1), groupOptions(2)], +})) +queryClient.prefetchQuery(groupOptions(23)) +queryClient.setQueryData(groupOptions(42).queryKey, newGroups) +``` + +[//]: # 'Example1' +[//]: # 'SelectDescription' + +You can still override some options at the component level. A very common and useful pattern is to create per-component `select` functions: + +[//]: # 'SelectDescription' +[//]: # 'Example2' + +```ts +// Type inference still works, so query.data will be the return type of select instead of queryFn + +const groupQuery = useQuery(() => ({ + ...groupOptions(1), + select: (data) => data.groupName, +})) +``` + +[//]: # 'Example2' diff --git a/docs/framework/solid/guides/request-waterfalls.md b/docs/framework/solid/guides/request-waterfalls.md index 6a4894e6097..fe3298a7429 100644 --- a/docs/framework/solid/guides/request-waterfalls.md +++ b/docs/framework/solid/guides/request-waterfalls.md @@ -1,17 +1,209 @@ --- id: request-waterfalls -title: Request Waterfalls +title: Performance & Request Waterfalls ref: docs/framework/react/guides/request-waterfalls.md -replace: - { - 'React': 'Solid', - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - 'useSuspenseQuery': 'useQuery', - 'useSuspenseQueries': 'useQueries', - } +replace: { 'React Query': 'Solid Query' } --- + +[//]: # 'AdvancedSSRLink' +[//]: # 'AdvancedSSRLink' +[//]: # 'DependentExample' + +```tsx +// Get the user +const userQuery = useQuery(() => ({ + queryKey: ['user', email], + queryFn: getUserByEmail, +})) + +const userId = () => userQuery.data?.id + +// Then get the user's projects +const projectsQuery = useQuery(() => ({ + queryKey: ['projects', userId()], + queryFn: getProjectsByUser, + // The query will not execute until the userId exists + enabled: !!userId(), +})) +``` + +[//]: # 'DependentExample' +[//]: # 'ServerComponentsNote1' +[//]: # 'ServerComponentsNote1' +[//]: # 'SuspenseSerial' +[//]: # 'SuspenseSerial' +[//]: # 'NestedIntro' + +Nested Component Waterfalls is when both a parent and a child component contains queries, and the parent does not render the child until its query is done. + +[//]: # 'NestedIntro' +[//]: # 'NestedExample' + +```tsx +import { Switch, Match } from 'solid-js' + +function Article(props) { + const articleQuery = useQuery(() => ({ + queryKey: ['article', props.id], + queryFn: getArticleById, + })) + + return ( + + + Loading article... + + + + + + + + ) +} + +function Comments(props) { + const commentsQuery = useQuery(() => ({ + queryKey: ['article-comments', props.id], + queryFn: getArticleCommentsById, + })) + + ... +} +``` + +[//]: # 'NestedExample' +[//]: # 'NestedHoistedExample' + +```tsx +import { Switch, Match, Show } from 'solid-js' + +function Article(props) { + const articleQuery = useQuery(() => ({ + queryKey: ['article', props.id], + queryFn: getArticleById, + })) + + const commentsQuery = useQuery(() => ({ + queryKey: ['article-comments', props.id], + queryFn: getArticleCommentsById, + })) + + return ( + + Loading article... + + + + + Loading comments... + + + + + + + ) +} +``` + +[//]: # 'NestedHoistedExample' +[//]: # 'NestedHoistedOutro' + +The two queries will now fetch in parallel. + +[//]: # 'NestedHoistedOutro' +[//]: # 'DependentNestedExample' + +```tsx +import { Switch, Match, For } from 'solid-js' + +function Feed() { + const feedQuery = useQuery(() => ({ + queryKey: ['feed'], + queryFn: getFeed, + })) + + return ( + + + Loading feed... + + + + {(feedItem) => { + if (feedItem.type === 'GRAPH') { + return + } + return + }} + + + + ) +} + +function GraphFeedItem(props) { + const graphQuery = useQuery(() => ({ + queryKey: ['graph', props.feedItem.id], + queryFn: getGraphDataById, + })) + + ... +} +``` + +[//]: # 'DependentNestedExample' +[//]: # 'ServerComponentsNote2' + +In this example, we can't trivially flatten the waterfall by just hoisting the query to the parent, or even adding prefetching. Just like the dependent query example at the beginning of this guide, one option is to refactor our API to include the graph data in the `getFeed` query. + +[//]: # 'ServerComponentsNote2' +[//]: # 'LazyExample' + +```tsx +import { lazy, Switch, Match, For } from 'solid-js' + +// This lazy loads the GraphFeedItem component, meaning +// it wont start loading until something renders it +const GraphFeedItem = lazy(() => import('./GraphFeedItem')) + +function Feed() { + const feedQuery = useQuery(() => ({ + queryKey: ['feed'], + queryFn: getFeed, + })) + + return ( + + + Loading feed... + + + + {(feedItem) => { + if (feedItem.type === 'GRAPH') { + return + } + return + }} + + + + ) +} + +// GraphFeedItem.tsx +function GraphFeedItem(props) { + const graphQuery = useQuery(() => ({ + queryKey: ['graph', props.feedItem.id], + queryFn: getGraphDataById, + })) + + ... +} +``` + +[//]: # 'LazyExample' +[//]: # 'ServerComponentsNote3' +[//]: # 'ServerComponentsNote3' diff --git a/docs/framework/solid/guides/scroll-restoration.md b/docs/framework/solid/guides/scroll-restoration.md index cb66c4b463c..55ceb296fe9 100644 --- a/docs/framework/solid/guides/scroll-restoration.md +++ b/docs/framework/solid/guides/scroll-restoration.md @@ -2,4 +2,5 @@ id: scroll-restoration title: Scroll Restoration ref: docs/framework/react/guides/scroll-restoration.md +replace: { 'React Router’s ScrollRestoration, ': '' } --- diff --git a/docs/framework/solid/guides/suspense.md b/docs/framework/solid/guides/suspense.md index 48dbd0881cc..412f113089c 100644 --- a/docs/framework/solid/guides/suspense.md +++ b/docs/framework/solid/guides/suspense.md @@ -25,14 +25,14 @@ const todoFetcher = async () => ) function SuspendableComponent() { - const query = useQuery(() => ({ + const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: todoFetcher, })) - // Accessing query.data directly inside a boundary + // Accessing todosQuery.data directly inside a boundary // automatically triggers suspension until data is ready - return
    Data: {JSON.stringify(query.data)}
    + return
    Data: {JSON.stringify(todosQuery.data)}
    } ``` diff --git a/docs/framework/solid/guides/updates-from-mutation-responses.md b/docs/framework/solid/guides/updates-from-mutation-responses.md index e11ae74270e..ffa1fb5c7fa 100644 --- a/docs/framework/solid/guides/updates-from-mutation-responses.md +++ b/docs/framework/solid/guides/updates-from-mutation-responses.md @@ -2,14 +2,49 @@ id: updates-from-mutation-responses title: Updates from Mutation Responses ref: docs/framework/react/guides/updates-from-mutation-responses.md -replace: - { - 'React': 'Solid', - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - } +replace: { 'hook': 'function' } --- + +[//]: # 'Example' + +```tsx +const queryClient = useQueryClient() + +const mutation = useMutation(() => ({ + mutationFn: editTodo, + onSuccess: (data) => { + queryClient.setQueryData(['todo', { id: 5 }], data) + }, +})) + +mutation.mutate({ + id: 5, + name: 'Do the laundry', +}) + +// The query below will be updated with the response from the +// successful mutation +const todoQuery = useQuery(() => ({ + queryKey: ['todo', { id: 5 }], + queryFn: fetchTodoById, +})) +``` + +[//]: # 'Example' +[//]: # 'Example2' + +```tsx +const useMutateTodo = () => { + const queryClient = useQueryClient() + + return useMutation(() => ({ + mutationFn: editTodo, + // Notice the second argument is the variables object that the `mutate` function receives + onSuccess: (data, variables) => { + queryClient.setQueryData(['todo', { id: variables.id }], data) + }, + })) +} +``` + +[//]: # 'Example2' diff --git a/docs/framework/solid/guides/window-focus-refetching.md b/docs/framework/solid/guides/window-focus-refetching.md index 1c5260fff3f..eaec584eec1 100644 --- a/docs/framework/solid/guides/window-focus-refetching.md +++ b/docs/framework/solid/guides/window-focus-refetching.md @@ -2,13 +2,19 @@ id: window-focus-refetching title: Window Focus Refetching ref: docs/framework/react/guides/window-focus-refetching.md -replace: - { - '@tanstack/react-query': '@tanstack/solid-query', - 'useMutationState[(]': 'useMutationState(() => ', - 'useMutation[(]': 'useMutation(() => ', - 'useQuery[(]': 'useQuery(() => ', - 'useQueries[(]': 'useQueries(() => ', - 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', - } +replace: { '@tanstack/react-query': '@tanstack/solid-query' } --- + +[//]: # 'Example2' + +```tsx +useQuery(() => ({ + queryKey: ['todos'], + queryFn: fetchTodos, + refetchOnWindowFocus: false, +})) +``` + +[//]: # 'Example2' +[//]: # 'ReactNative' +[//]: # 'ReactNative' diff --git a/examples/solid/offline/.eslintrc.cjs b/examples/solid/offline/.eslintrc.cjs new file mode 100644 index 00000000000..cca134ce166 --- /dev/null +++ b/examples/solid/offline/.eslintrc.cjs @@ -0,0 +1,6 @@ +// @ts-check + +/** @type {import('eslint').Linter.Config} */ +const config = {} + +module.exports = config diff --git a/examples/solid/offline/README.md b/examples/solid/offline/README.md new file mode 100644 index 00000000000..310f37f62fd --- /dev/null +++ b/examples/solid/offline/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` +- `npm run start` diff --git a/examples/solid/offline/index.html b/examples/solid/offline/index.html new file mode 100644 index 00000000000..7d9b4f7c51c --- /dev/null +++ b/examples/solid/offline/index.html @@ -0,0 +1,16 @@ + + + + + + + + Solid App + + + +
    + + + + diff --git a/examples/solid/offline/package.json b/examples/solid/offline/package.json new file mode 100644 index 00000000000..f141757e0c0 --- /dev/null +++ b/examples/solid/offline/package.json @@ -0,0 +1,28 @@ +{ + "name": "@tanstack/query-example-solid-offline", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/query-async-storage-persister": "^5.90.22", + "@tanstack/solid-query": "^5.90.23", + "@tanstack/solid-query-devtools": "^5.91.3", + "@tanstack/solid-query-persist-client": "^5.90.22", + "msw": "^2.6.6", + "solid-js": "^1.9.7" + }, + "devDependencies": { + "typescript": "5.8.3", + "vite": "^6.4.1", + "vite-plugin-solid": "^2.11.6" + }, + "msw": { + "workerDirectory": [ + "public" + ] + } +} diff --git a/examples/solid/offline/src/App.tsx b/examples/solid/offline/src/App.tsx new file mode 100644 index 00000000000..97e0456906b --- /dev/null +++ b/examples/solid/offline/src/App.tsx @@ -0,0 +1,208 @@ +import { For, Match, Show, Switch, createSignal } from 'solid-js' +import { MutationCache, QueryClient, useQuery } from '@tanstack/solid-query' +import { SolidQueryDevtools } from '@tanstack/solid-query-devtools' +import { PersistQueryClientProvider } from '@tanstack/solid-query-persist-client' +import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' + +import * as api from './api' +import { movieKeys, useMovie } from './movies' + +const persister = createAsyncStoragePersister({ + storage: window.localStorage, +}) + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: 1000 * 60 * 60 * 24, // 24 hours + staleTime: 2000, + retry: 0, + }, + }, + // configure global cache callbacks to show notifications + mutationCache: new MutationCache({ + onSuccess: (data: any) => { + showNotification(data.message, 'success') + }, + onError: (error) => { + showNotification(error.message, 'error') + }, + }), +}) + +// we need a default mutation function so that paused mutations can resume after a page reload +queryClient.setMutationDefaults(movieKeys.all(), { + mutationFn: async ({ id, comment }: { id: string; comment: string }) => { + // to avoid clashes with our optimistic update when an offline mutation continues + await queryClient.cancelQueries({ queryKey: movieKeys.detail(id) }) + return api.updateMovie(id, comment) + }, +}) + +function showNotification(message: string, type: 'success' | 'error') { + const el = document.createElement('div') + el.style.cssText = ` + position: fixed; top: 16px; right: 16px; z-index: 9999; + padding: 12px 24px; border-radius: 4px; color: white; + background: ${type === 'success' ? '#22c55e' : '#ef4444'}; + animation: fadeIn 0.3s ease-in; + ` + el.textContent = message + document.body.appendChild(el) + setTimeout(() => el.remove(), 3000) +} + +export default function App() { + return ( + { + // resume mutations after initial restore from localStorage was successful + queryClient.resumePausedMutations().then(() => { + queryClient.invalidateQueries() + }) + }} + > + + + + ) +} + +function Movies() { + const [page, setPage] = createSignal< + { type: 'list' } | { type: 'detail'; movieId: string } + >({ type: 'list' }) + + return ( + + + setPage({ type: 'detail', movieId })} + /> + + + setPage({ type: 'list' })} + /> + + + ) +} + +function List(props: { onSelectMovie: (movieId: string) => void }) { + const moviesQuery = useQuery(() => ({ + queryKey: movieKeys.list(), + queryFn: api.fetchMovies, + })) + + return ( + + Loading... + +
    +

    Movies

    +

    + Try to mock offline behavior with the button in the devtools. You + can navigate around as long as there is already data in the cache. + You'll get a refetch as soon as you go online again. +

    + +
    + Updated at: {new Date(moviesQuery.data!.ts).toLocaleTimeString()} +
    + +
    fetching...
    +
    +
    +
    +
    + ) +} + +function Detail(props: { movieId: string; onBack: () => void }) { + const { comment, setComment, updateMovie, movieQuery } = useMovie( + props.movieId, + ) + + function submitForm(event: SubmitEvent) { + event.preventDefault() + + updateMovie.mutate({ + id: props.movieId, + comment: comment(), + }) + } + + return ( + + Loading... + +
    + { + e.preventDefault() + props.onBack() + }} + > + Back + +

    Movie: {movieQuery.data!.movie.title}

    +

    + Try to mock offline behavior with the button in the devtools, then + update the comment. The optimistic update will succeed, but the + actual mutation will be paused and resumed once you go online again. +

    +

    + You can also reload the page, which will make the persisted mutation + resume, as you will be online again when you "come back". +

    +

    +