Replies: 37 comments 138 replies
-
+1 for this change, and great article. It always seemed like an unnecessary footgun that I try to catch in PRs. For
Problem with these approaches:
As for UI notifications, regardless of whether its For what it's worth, here are some guidelines I've set down for myself and my team. These are not hard-and-fast rules, but rather a mental model of what should be called where, and who should be doing what. Service Layer PatternLayer 1: Pure network call
export const fetchById= (id: string) => {
try {
const data = await apiClient.getById(id);
return mapBadAPIContractToGoodClientContract(data)
} catch (e: unknown) {
throw ServiceErrorFactory.create(e)
}
}; Layer 2: API State (Hook)
export const useFetchById= (id: string) => {
const cacheKey = [CacheKey.FetchById, id]
return useQuery<FetchByIdResponse, ServiceError>(
cacheKey,
() => fetchById(id),
{
enabled: Boolean(id)
},
)
} Layer 3: Page Hook (Optional)
export const usePageWithId= (id: string) => {
const {
data,
error,
isLoading,
isSuccess,
isError
} = useFetchById(id)
/**
* Optionally map the network error to a React component
*/
let errorFallbackComponent: ReactNode | undefined
if (isError) {
errorFallbackComponent = mapErrorToFallbackComponent(error?.code)
}
/**
* Optionally map response data to Component page props
*/
let props: PageWithIdProps| undefined
if (data) {
props = mapMyDataToMyComponentProps(data)
}
/**
* Optionally take action in the event of a successful call
*/
useEffect(() => {
if (isSucces) {
showToastNotification('Success!')
}
}, [isSuccess])
/**
* Optionally take action in the event of a failed call
*/
useEffect(() => {
if (isError) {
showToastNotification('Error!')
}
}, [isError])
return { props, isLoading, isError, errorFallbackComponent }
} Layer 4: Page Building
export const PageWithId= ({id}: {id: string}) => {
const {
props,
isError,
isLoading,
errorFallbackComponent,
} = usePageWithId(id);
// Don't have Layer 3? Just call your useQuery hook normally
// const {
// data,
// error,
// isLoading,
// isSuccess,
// isError
// } = useFetchById(id)
if (isLoading) {
return <Loading />;
}
if (isError) {
return errorFallbackComponent;
}
return <PageTemplate {...props} />;
}; Okay, this turned out longer than I anticipated, but I just wanted to provide a scalable pattern in the event this change kicks up more dust. |
Beta Was this translation helpful? Give feedback.
-
What about "useQueries"? If the callbacks are removed, is it even possible to write a useEffect that reacts to the fetched data change? I have a specific case where the number of queries is dynamic and I perform some calculations based on the index of the query, that was successful. |
Beta Was this translation helpful? Give feedback.
-
In the blogpost you mentioned BTW I don't use these callbacks for queries in my app, only in mutations, but I can see the use-case. |
Beta Was this translation helpful? Give feedback.
-
My team currently uses https://gist.github.com/Wyatt-SG/2d7f93f50956954857bd8e6b3f447275 With this change what would be your recommended pattern? |
Beta Was this translation helpful? Give feedback.
-
Instead of removing callbacks from useQuery. Can't we just compare the queryFn returned data and if data are different then call onSucess? Like adding this where ever the callback is calling |
Beta Was this translation helpful? Give feedback.
-
@TkDodo So currently we use the callbacks to update query parameters after making sure that it won't create a rendering loop.
We also use it to update our redux store when certain data is required by client side state or thunks For example organisation data that we need in redux state
Do you think there are better ways to do these? The useEffect loop ads more render cycles and dependency gotcha's to this whole process. How else would you recommend to do this? |
Beta Was this translation helpful? Give feedback.
-
This is only for |
Beta Was this translation helpful? Give feedback.
-
I'm probably a bit late to the party, but I still wanted to contribute. For some background, the app I'm working on is a Vue3 SPA learning platform that has customized content for each "environment" (usually one per company). We use UsageWe have a handful of queries that use these callbacks, so not wide-spread usage, which is good news for when we need to migrate. Here is a list of all the things that currently happen in our
|
Beta Was this translation helpful? Give feedback.
-
It would be awesome to have a lint rule or something so that it's possible to disable these callbacks on a v4 version before upgrading to v5, since this seems like one of the more complicated changes requires other than simple API changes |
Beta Was this translation helpful? Give feedback.
-
I have a case where I have to use some hooks in my
Can you suggest some solution to this issue? |
Beta Was this translation helpful? Give feedback.
-
What would be the correct way to handle the errors? I have a I'm currently only using |
Beta Was this translation helpful? Give feedback.
-
I found another bug in some code of mine that could have been prevented by this, so I agree that this is a footgun and should be removed. function usePagination(key, page) {
const queryClient = useQueryClient();
return useQuery({
key,
queryFn: doCall(page),
onSuccess() {
queryClient.prefetch(nextPageKey, () => doCall(page + 1))
},
});
} When I increased the stale time on this prefetching stoped to work for every other page, since the next page queries where not stale anymore, they did not to the request anymore, not triggering the onSuccess anymore. |
Beta Was this translation helpful? Give feedback.
-
I have a case where I have two useQuery hooks, one is connected to a endpoint which returns a list, the other an endpoint which returns a single object. In both cases each item returned also contains a version property which I use to understand if any object has chanaged. Currently I use the onSuccess callbacks in each useQuery to update the queryData of the other key, but only when the version has changed. In light of the planned removal of onSuccess how would be best to migrate these? Simplified example: export const useUsers = () => {
const queryClient = useQueryClient();
return useQuery<User[], Error>({
queryKey: ['user','list'],
queryFn: async () => getUsers(),
onSuccess: (data) => {
data.forEach((user) => {
const cacheKey = ['user', { id: user.id }];
const oldCache = queryClient.getQueryData<User>(cacheKey);
if (!oldCache || user.version > oldCache.version) {
console.debug('Updated cache for', cacheKey);
queryClient.setQueryData(cacheKey, user);
});
}
});
};
export const useUser = (userId: string | undefined) => {
const queryClient = useQueryClient();
return useQuery<User, Error>({
queryKey: ['user', { id: userId }],
queryFn: async () => getUser(userId),
enabled: Boolean(userId),
onSuccess: (user) => {
const cacheKey = ['user','list'];
const oldCache = queryClient.getQueryData<User[]>(cacheKey);
if (!oldCache || user.version > oldCache.version) {
console.debug('Updated cache for', ...cacheKey);
queryClient.setQueryData(cacheKey, (old: User[] = []) => [...old.filter((u) => u.id !== user.id), user]);
}
}
});
}; |
Beta Was this translation helpful? Give feedback.
-
One thing I use Take this example, If I am getting all the playlists from the server, I may as well cache the individual ones to avoid having to pull them later. How would I go about doing this in the API? |
Beta Was this translation helpful? Give feedback.
-
Sorry if I am missing something but I couldn't find a suggested pattern for my use case. here I saw that we could use the local
I only want this logic to apply to this query and none of my other queries. I already have a global |
Beta Was this translation helpful? Give feedback.
-
The main reason we use react-query anyways, apart from caching, is to avoid having to use What will happen is people will eventually have to do workarounds to maintain the availability of those callbacks, for example something like this: import { UseQueryOptions, useQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
interface Callbacks {
onSuccess?: (data: any) => void;
onError?: (error: any) => void;
}
export const useQueryWithCallbacks = <T>(options: UseQueryOptions<T> & Callbacks) => {
const result = useQuery(options);
useEffect(() => {
if (result.isSuccess && options?.onSuccess) {
options.onSuccess(result.data);
}
}, [result, options]);
useEffect(() => {
if (result.isError && options?.onError) {
options.onError(result.error);
}
}, [result, options]);
return result;
};
...
const { data, isLoading, isError } = useQueryWithCallbacks(
{
queryKey: ['users'],
queryFn: listUsers,
onError: () => toast.error('error'),
onSuccess: () => toast.success('success')
}
); This makes V5 behave similarly to V4, but then you don't have to add an extra |
Beta Was this translation helpful? Give feedback.
-
So... what do you recommend doing instead? If you simply want to log out every response. |
Beta Was this translation helpful? Give feedback.
-
Hi @TkDodo might be late but i'd like to post question here to be prepare for upcoming change in v5 Example
Then to follow upcoming changes can we use
Or do some There is any things with negative effect here? I read the blog and see that we can replace it with useEffect but it might there's more line of codes that the simply thing I need. Thank you in advance. |
Beta Was this translation helpful? Give feedback.
-
Here's a hack I'm using to get around this update: // App.tsx or wherever you define your `<QueryClientProvider client={queryClient}>`
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
captureException(error);
if (query.meta?.onError) {
return query.meta.onError(error, query);
}
},
}),
}); // Example query hook that wraps `useQuery`
export const useExampleQuery = () => {
return useQuery({
queryKey: ["exampleQuery"],
queryFn: () => fetchExampleData(),
meta: {
onError: (error, query) => {
toast.error("Something went wrong :(");
},
},
});
}; // react-query.d.ts
import "@tanstack/react-query";
// Extends the QueryMeta interface from react-query to include our custom notification message types
declare module "@tanstack/react-query" {
export interface QueryMeta {
/**
* Exposes a `meta.onError` event to `useQuery` options.
*/
onError: QueryCacheConfig["onError"];
}
} I think this pattern makes a lot of sense... I mostly just want to still be able to present a custom toast depending on the error in my Is there any danger to using a pattern like this? |
Beta Was this translation helpful? Give feedback.
-
I currently use My context is an entity creation/update form (let's say a It's not great and the removal of callbacks from Any ideas? Thanks! |
Beta Was this translation helpful? Give feedback.
-
I hoped there would be solutions for one time onSuccess/onError/onSettled properties per fetch, since global function does not really sounds great for my use case - I have a lot of api routes with a lot of unique success and error messages with additional logic, so I pretty much write my code like this: const queryFn = (props: Props) => {
try {
const data = await fetchApi(props);
const code = data.code;
switch(code) {
...
}
return data;
} catch (error) {
if (isAxiosError<BaseAPIResponse>(error) && error.response) {
const httpCode = error.response.status;
const data = error.response.data;
const code = data.code;
switch (httpCode) {
...
}
switch (code) {
...
}
return Promise.reject(error)
}
}
}
const myQuery = (props: Props) => useQuery(queryKey:['some-key', props], queryFn); It kinda works, but I am not fully satisfied (still best of what I had worked with before).
|
Beta Was this translation helpful? Give feedback.
-
Thanks for taking the time to help answer our migration questions. I read through your blog post but it's still unclear to me what the potential drawbacks are to using a Prior to query v5, I was using the onSuccess callback to dispatch state changes to various reducers. So I've refactored my code now to use a custom wrapper hook with a
I believe this follows the workaround example you provide, but what are the drawbacks to this approach? |
Beta Was this translation helpful? Give feedback.
-
I support this decision to remove callbacks, but am struggling converting a codebase to v5 that currently uses onSuccess to post some telemetry (status, response time, etc.) for the various API calls made. The issue I have is that there is a dynamic property in the telemetry which notes whether the user is waiting for the API call or not - the majority are done in the background, but there are some instances where refetch is called and the user is waiting for the data to be updated. I can see how I can switch to using the global onSuccess callback, passing the query info to the callback via meta, but I can't pass the 'userWaiting' value through meta since it won't change after initially being set. Is there any way to pass a value that changes through to the global onSuccess callback, without having 2 versions of each query (ie. have the information within the querykey)? |
Beta Was this translation helpful? Give feedback.
-
Is there a recommended way to update form data from a callback without using I'm using This is what I'm currently doing: function Component({ form }: { form: FormReturnType }) {
const [nameWasChanged, setNameWasChanged] = useState(false);
const currentName = form.watch("name");
const currentProfession = form.watch("profession");
const query = useQuery({
queryKey: ["profession", currentName],
queryFn: getProfessionForName,
enabled: nameWasChanged,
onSuccess: (fetchedProfession: string) => {
form.setValue("profession", fetchedProfession);
}
});
return (
<Select
label="Name"
options={[]}
onChange={(newName: string) => {
form.setValue("name", newName);
setNameWasChanged(true);
}}
/>
);
} Also, I'm actually using TRPC to handle the queries, so it's actually const query = trpc.profession.useQuery(currentName, { /* options */ }); which means that |
Beta Was this translation helpful? Give feedback.
-
For people who want to have type QueryEvents<RespT, ErrT> = {
onSuccess: (resp: RespT) => any;
onError: (resp: ErrT) => any;
};
function useQueryEvents<RespT, ErrT>(query: UseQueryResult<RespT, ErrT>, callbacks: Partial<QueryEvents<RespT, ErrT>>) {
const { onSuccess, onError } = callbacks;
React.useEffect(() => {
if (query.data && onSuccess) {
onSuccess(query.data);
}
}, [query.data, onSuccess]);
React.useEffect(() => {
if (query.error && onError) {
onError(query.error);
}
}, [query.error, onError]);
} With this hook const MyComponent: React.FC = () => {
const userQuery = useQuery(...)
useQueryEvents(userQuery, {
onSuccess: (user) => console.log('User has been fetched: ', user),
onError: (err) => console.log('An error happened:', err.message),
})
return <div>
...
</div>
} |
Beta Was this translation helpful? Give feedback.
-
You violate the O-principle of SOLID principles. And you offer solutions that violate the KISS principle. You have solved a rare problem and broken the most convenient API. In a normal world, this could not happen, because it is hard to imagine that this could happen in professional communities. |
Beta Was this translation helpful? Give feedback.
-
In my usecase, we're using react-query with MVVM architecture. We changed this way when migrating to last version of react-query export default function HomeViewModel() {
const { user } = useUserStore()
const storyRepository = useMemo(() => createUserStoryRepository(createUserStoryService()), [])
const fetchAllUserStoryUseCase = useMemo(
() => createFetchAllUserStoryUseCase(storyRepository),
[storyRepository],
)
const homeViewState = useUserStories()
function useUserStories() {
const userId = user?.userId
const { data, error, isLoading, isError } = useQuery({
staleTime: 3 * 60 * 1000,
queryKey: ["user-stories", userId],
queryFn: () => fetchAllUserStoryUseCase.execute(userId!),
enabled: !!user?.userId,
})
// Handle loading state
if (isLoading) {
return { loading: true }
}
// Handle error state
if (isError && error) {
console.error(error)
return { loading: false, storyList: [] }
}
// Handle successful data fetching
if (data) {
const formattedData = data.map((story) => ({
id: story.id,
title: story.title,
coverImageUrl: story.cover,
}))
return {
loading: false,
storyList: formattedData,
}
}
}
return {
homeViewState,
}
} |
Beta Was this translation helpful? Give feedback.
-
I'm curious to understand why the QueryCache doesn't offer a way to subscribe to errors through |
Beta Was this translation helpful? Give feedback.
-
late to the party, but still: Why not add new callbacks: "onCacheSuccess" and "onCacheError"? no misleading, better API, does not require write same thing but manually and reuse query client everywhere |
Beta Was this translation helpful? Give feedback.
-
I wanted to write an RFC, but after I announced it on twitter, I decided to make a blogpost. This should cover most of the questions around that topic:
https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose
Beta Was this translation helpful? Give feedback.
All reactions