Why use jotai-tanstack-query? #2730
Replies: 6 comments 15 replies
-
Follow up: If I wanted to go in the opposite direction, a tanstack query result that I wanted to store as a derived atom, how would I do that. Specifically this might be useful if I have a really large piece of async state stored in a react-query result that I want to derive transformations and pieces from. const { isPending, isError, data, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
// how do I properly get "first two todos" stored in an atom derived from `data` here? |
Beta Was this translation helpful? Give feedback.
-
I would like to hear from @kalijonn , but my perspective is |
Beta Was this translation helpful? Give feedback.
-
I felt like this needed some explanation and some examples, so I wrote about this. https://blog.thiskali.com/jotai-tanstack-query/ You can use @tanstack/react-query with jotai, without using jotai-tanstack-query. I believe jotai-tanstack-query has far better DX when the state is intertwined. About the follow up question, you have to use useEffect and setAtom to get tanstack query's result into an atom. |
Beta Was this translation helpful? Give feedback.
-
I've given a lot of thought to this question over the past several months, I'll try my best to describe my thoughts on this that I hope complement @kalijonn's blog post. I'm also curious what others have to say on the topic. @tanstack/query-* could be an atomic state management system.I ran into the following probem: when two components both use an inline useQuery that share the same queryKey but returns or throws different values in their queryFn. In this case TypeScript will misrepresent the types of data and error of one of the two useQuery calls. Please consider the following: function ComponentA() {
const { data, error } = useQuery({
queryKey: ['students'],
queryFn: async () => {
const response = await fetchStudents()
if (response.status !== 200) {
throw response.body
}
return response.body
})
...
}
function ComponentB() {
const { data, error } = useQuery({
queryKey: ['students'],
queryFn: async () => {
const response = await fetchStudents()
if (response.status !== 200) {
throw new Error('Unable to fetch students')
}
return response.body.students
})
...
} What is the type of data and error? In ComponentA, it is typeof response.body and typeof response.body. In ComponentB, it is typeof response.body.students and Error class instance. But they both can't be right, right? Since ComponentA and ComponentB share the same QueryClient instance, their actual type (not the one TypeScript reports) will depend on whichever component runs first, and first after invalidateQueries invocation for ['students']. This is a problem. Local changes should not cause global breaking side-effects. We must centralize. To solve this problem I considered many solutions:
Eventually I settled on centralizing the query options object. Even better is that @tanstack/query-react supports this pattern with the query options utility! import { queryOptions } from '@tanstack/react-query'
function studentsOptions() {
return queryOptions({
queryKey: ['students'],
queryFn: async () => {
const response = await fetchStudents()
if (response.status !== 200) {
throw response.body
}
return response.body
})
} A few observations of this pattern:
Query Options looks a lot like jotai atoms. Interesting. @tanstack/query-* idiomatic main difference with jotai - colocationWhether you centralize on hooks, queryOptions, queryKeys, or you choose not to centralize; tanstack query uses the co-location pattern. The individual components that call useQuery are treated separately with some handwaving that the logic for a given queryKey is considered the same across the app. Colocation allows component props to be integrated locally into the business logic, while the handwaving allows us to cheat and pretend its valid everywhere. This hack makes the ergonomics of tanstack query very easy, but it means that tanstack query isn't truely a global state management solution. We need another hack to make it compatible with jotai. function Student({ id }: { id: number }) {
const { data } = useQuery({
queryKey: ['student', { id }],
queryFn: ({ queryKey }) => {
const response = await fetchStudent({ id: queryKey[1].id })
if (response.status !== 200) {
throw response.body
}
return response.body
})
...
} How would you pair colocation with jotai-tanstack-query?Despite the fundamental difference, there are a few ways to handle this:
import { atomFamily } from 'jotai/utils';
import { atomWithQuery } from 'jotai-tanstack-query';
const studentAtomFamily = atomFamily((id) => atomWithQuery({
queryKey: ['student', id],
queryFn: ({ queryKey }) => {
const response = await fetchStudent({ id: queryKey[1].id })
if (response.status !== 200) {
throw response.body
}
return response.body
},
}));
function StudentComponent({ id }) {
const [studentData, { isLoading, error }] = useAtom(studentAtomFamily(id));
...
}; TODO: errors thrown in an atomFamily should propagate to QueryErrorResetBoundary.
import { atomFamily } from 'jotai/utils';
import { atomWithQuery } from 'jotai-tanstack-query';
const studentIdAtom = atom(null);
atomWithQuery((get) => {
const id = get(studentIdAtom)
return {
queryKey: ['student', id],
queryFn: ({ queryKey }) => {
const response = await fetchStudent({ id: queryKey[1].id })
if (response.status !== 200) {
throw response.body
}
return response.body
},
}
});
function StudentBaseComponent({ id }: { id: number }) {
const store = useStore()
useMemo(() => {
store.set(studentIdAtom, id)
}, [store, id])
const [studentData, { isLoading, error }] = useAtom(studentIdAtom);
...
};
function StudentComponent({ id }: { id: number }) {
return (
<ScopeProvider atoms={[studentIdAtom]}>
<StudentBaseComponent key={id} id={id} />
</ScopeProvider>
);
};
|
Beta Was this translation helpful? Give feedback.
-
@tylerlaws0n Hm, no. Setting atom values in useEffect (or atomEffect) is the thing I want to avoid the most. It's valid, but it's a last resort. |
Beta Was this translation helpful? Give feedback.
-
To follow up on this tweet from tkdodo: https://x.com/TkDodo/status/1835240510102868169 I think the concept of "derived state" is not practically solved by storing the entire app state as atoms. Sure, you can use I think I'd rather have const makeUseDerivedAtom = <Args extends any[], Derived>(
fn: (...args: Args) => Derived
): ((...args: Args) => Derived) => {
const dAtom = atom<Derived>();
return (...args: Args): Derived => {
const [derivedState, setDerivedState] = useAtom(dAtom);
useEffect(() => {
setDerivedState(fn(...args));
}, args);
return derivedState!;
};
};
const useRepeatedName = makeUseDerivedAtom((start: number, name: string) =>
`${name}\n`.repeat(start)
);
const nameRepeated = useRepeatedName(someNumber, router.params.name) I can see how it would be nice to use The alternative would be to mirror the url state as atoms as well, so that they are available with the I fully agree with this from Dai-shi:
That being said, in the interest of keeping complexity to a minimum, while getting the benefits of a single derived atom, I think |
Beta Was this translation helpful? Give feedback.
-
I was going to post this on https://github.com/jotaijs/jotai-tanstack-query , but there is no discussion board over there.
A question about using Jotai with react query:
jotai-tanstack-query seems specifically targeted at using atoms as deps for the queryKey field docs
How is this first snippet better/different from the second?
with jotai-tanstack-query:
With using @tanstack/react-query directly:
Asking this because it feels like I am missing something. If it is just a matter of DX/ergonomics (I get that), but I am specifically interested in if there are other considerations for why to create that extension.
Thanks for any feedback in advance
Beta Was this translation helpful? Give feedback.
All reactions