T
TanStack•2w ago
wise-white

UseQuery loading spinner showing right away! :(

hey guys im running into an issue where my useQuery ispending state is showing right away! even though im using enabled: false! im triggering the query to run manually by using refetch() but for some reason it shows the spinner instantly even though i havent called refetch() yet heres some context: would gladly appreciate some help
const {
data: subreddits = [],
isPending: isSubredditsPending,
error: subredditsError,
refetch,
} = useSearchSubreddits(authFetch, isAuth?.id, searchTerm);

// custom hook
export function useSearchSubreddits(authFetch, userId, searchTerm) {
return useQuery({
queryKey: ["searchSubreddits", userId],
queryFn: async ({ signal }) => {
const res = await authFetch(
`${import.meta.env.VITE_SERVER_URL}/api?q=${searchTerm.trim()}`,
{ signal },
);
const data = await res.json();
if (!res.ok) {
throw new Error("Failed to search subreddits");
}
return data;
},
enabled: false,
staleTime: 0,
gcTime: 0,
});
}

// loading state check
{/* Subreddit List */}
<div className="overflow-y-auto flex flex-col flex-1 p-4 text-gray-500">
{isSubredditsPending && searchTerm.trim() ? (
// Loader
<div className="flex justify-center items-center py-8">
<ImSpinner className="animate-spin text-2xl" />
</div>
const {
data: subreddits = [],
isPending: isSubredditsPending,
error: subredditsError,
refetch,
} = useSearchSubreddits(authFetch, isAuth?.id, searchTerm);

// custom hook
export function useSearchSubreddits(authFetch, userId, searchTerm) {
return useQuery({
queryKey: ["searchSubreddits", userId],
queryFn: async ({ signal }) => {
const res = await authFetch(
`${import.meta.env.VITE_SERVER_URL}/api?q=${searchTerm.trim()}`,
{ signal },
);
const data = await res.json();
if (!res.ok) {
throw new Error("Failed to search subreddits");
}
return data;
},
enabled: false,
staleTime: 0,
gcTime: 0,
});
}

// loading state check
{/* Subreddit List */}
<div className="overflow-y-auto flex flex-col flex-1 p-4 text-gray-500">
{isSubredditsPending && searchTerm.trim() ? (
// Loader
<div className="flex justify-center items-center py-8">
<ImSpinner className="animate-spin text-2xl" />
</div>
i previously had it as just isSubredditsPending that showed the spinner instantly but then i added searchTerm.trim() and it didnt show it instantly but now as soon as i type a letter it shows the spinner even though i have clicked search button yet šŸ™ heres my manual refetch call
// search subreddits
const handleSearch = async () => {
if (!searchTerm.trim()) return;
await refetch();
setSearchTerm("");
};
// search subreddits
const handleSearch = async () => {
if (!searchTerm.trim()) return;
await refetch();
setSearchTerm("");
};
the search button triggers this! i need some guidance as to what im doing wrong and how can i fix this šŸ™
20 Replies
wise-white
wise-whiteOP•2w ago
is it something to do with search term ? as it updates on every keystroke ! but i didnt add it to the queryKey
sensitive-blue
sensitive-blue•2w ago
You need to use isLoading over isPending here, but also please put searchTerm into the queryKey. You don't need refetch
sensitive-blue
sensitive-blue•2w ago
Disabling/Pausing Queries | TanStack Query React Docs
If you ever want to disable a query from automatically running, you can use the enabled = false option. The enabled option also accepts a callback that returns a boolean. When enabled is false: If the...
wise-white
wise-whiteOP•2w ago
tysm! ill look into this !! what im trying to do is after i input the searchterm and click search button it should trigger the fetch! and once results are in i want the input field to clear so search term should be "" then and then when i input anything i should be able to filter through the results i already fetched! does that make sense ?
fair-rose
fair-rose•2w ago
To do this, typically you would have 1. query accepts all dependencies (in this case, search term in the query key) 2. search term is either a local or global state that is accessed in your component and passed to your query 3. when you search, update the search term which will automatically cause the query to fetch 4. use isSuccess or something other value related to the query state to determine if you need to reset your input state to "".
const [search, setSearch] = useState();

const { isLoading, isSuccess } = useQuery({
queryKey: ["xQuery", search],
...
})

return (
<>
{!search && firstInput
{search && secondInput}
<button />
</>
)
const [search, setSearch] = useState();

const { isLoading, isSuccess } = useQuery({
queryKey: ["xQuery", search],
...
})

return (
<>
{!search && firstInput
{search && secondInput}
<button />
</>
)
wise-white
wise-whiteOP•2w ago
Tysm this is interesting what’s the first input and second input here ? Are they pieces of state ?
fair-rose
fair-rose•2w ago
Uh, sorry for not being more explicit. This is just based on the requirements you said of needing an input to input a search term, and then another input (or the same one) to then filter based on the results. I didn't want to go too in depth about the input fields because you may be using a form library, an html form or just manual useStates for the input states. So in this case - firstInput = input to enter a search term and then click search button which updates the search state with the value in the input field - secondInput = input you can use to filter through the results. If you need to reset your input state to "", the way you would do this depends on how your form/inputs work as to whether your form library offers a way to do this, using a react key to reset the input when the key changes, or using a useEffect to reset the input when the API response changes.
wise-white
wise-whiteOP•2w ago
Okay so this is what I ended up doing… Btw I don’t have 2 separate input fields And I’m not using a form library… So what I did was removed searchterm from the query key so in that case I won’t have caching available… I guess I wouldn’t need caching for this but it’d have heen great to have caching… anyways What I did is when I type in the input field and click search… I then manually run refetch() to run the query as my enabled flag is false permanently ! After the query runs and completes I reset the search term to ā€œā€ after awaiting the refetch() I then can use the same input field to filter the results based on the searchterm I type out filtering the results live as I type and when it’s ā€œā€ it displays all the results ! Does it make sense? When I close the dialog box with the results I run queryClient.removeQueries(queryKey) to reset the dialog box whenever I reopen so it doesn’t display the results of the previous searchterm Now this seems to work but at the expense of having no caching… so whenever I input the same searchterm and click search it will do another refetch to fetch the results of the same searchterm again Now I think this is fine but I’m not sure if this is the best practice… also it would be great to have caching per search term so I can keep the results for a specific search term for like 2 hours ? In case I search the same searchterm again ? But I don’t know how to do that while keeping a SINGLE input field and also being able to filter using that And also being able to remove the results when I close the dialog so it doesn’t display the previous results when I reopen it !
fair-rose
fair-rose•2w ago
So pretty much, if you are using react, and you are in a component, you should never need to call the query functions directly for fetching data (expect invalidate, prefetch, etc), there are exceptions of course but not typically. Avoiding the queryKey and calling refetch is similar to avoiding your deps array in a useEffect, and then manually doing the logic that is in the useEffect when state changes in your onClick handler. It can be the correct way, but if you already have the useEffect, typically not. Given your requirements 1. use 1 input field for initial search and subsequent search 2. clear search term on dialog close 3. have caching for searchTerm API requests Here is a minimal and pretty poor implementation of what you would want that sort of follows the useQuery approach.
// dialog.tsx

// ensure dialog clears your component state when it closes, if it doesn't unmount your component when it is closed, you can manually do this with a key which will reset component state
<Dialog isOpen={isOpen} onClose={onClose}>
<MyDialog key={String(isOpen)} />
</Dialog>
// dialog.tsx

// ensure dialog clears your component state when it closes, if it doesn't unmount your component when it is closed, you can manually do this with a key which will reset component state
<Dialog isOpen={isOpen} onClose={onClose}>
<MyDialog key={String(isOpen)} />
</Dialog>
// myDialog.tsx
const MyDialog = () => {
// store the search term (pre-search) and filter (post search) values for the input. you can use hasSearch to determine which state you are in, and make a discriminated union in typescript if you want to be really type safe
const [state, setState] = useState({
searchTerm: "",
hasSearched: false,
filter: null
});

// query will only run when you have searched, and then your searchTerm isn't going to change again so will be fine
const { isSuccess, data } = useQuery({
queryKey: ["my-query", state.searchTerm],
queryFn: () => ...,
enabled: !!state.hasSearched,
// use your filter value from the input after search to filter down the results locally. could consider useCallback if this function is expensive
select: (data) => customFilterBasedOnUserInput(data, state.filter)
})

// pass `onChange` to the correct state based on the component searched state
const handleOnChange = (e) => {
if (state.hasSearched) {
setState((s) => ({ ...s, filter: e.target.value }))
} else {
setState((s) => ({ ...s, searchTerm: e.target.value }))
}
}

// we should just need to change whether or not we have searched at this point (more to next state of this component)
const onClick = () => {
setState((s) => ({ ...s, hasSearched: true });
}

return (
<>
// key will change when your hasSearched changes.
<input key={String(state.hasSearched)} onChange={onChange} />
<button onClick={onClick}>search</button>
</>
);
}
// myDialog.tsx
const MyDialog = () => {
// store the search term (pre-search) and filter (post search) values for the input. you can use hasSearch to determine which state you are in, and make a discriminated union in typescript if you want to be really type safe
const [state, setState] = useState({
searchTerm: "",
hasSearched: false,
filter: null
});

// query will only run when you have searched, and then your searchTerm isn't going to change again so will be fine
const { isSuccess, data } = useQuery({
queryKey: ["my-query", state.searchTerm],
queryFn: () => ...,
enabled: !!state.hasSearched,
// use your filter value from the input after search to filter down the results locally. could consider useCallback if this function is expensive
select: (data) => customFilterBasedOnUserInput(data, state.filter)
})

// pass `onChange` to the correct state based on the component searched state
const handleOnChange = (e) => {
if (state.hasSearched) {
setState((s) => ({ ...s, filter: e.target.value }))
} else {
setState((s) => ({ ...s, searchTerm: e.target.value }))
}
}

// we should just need to change whether or not we have searched at this point (more to next state of this component)
const onClick = () => {
setState((s) => ({ ...s, hasSearched: true });
}

return (
<>
// key will change when your hasSearched changes.
<input key={String(state.hasSearched)} onChange={onChange} />
<button onClick={onClick}>search</button>
</>
);
}
Personally, I think the "single" input field is a misnomer. You want one input field visually, but this does not need to be the case in code. You can quite easily make an input/button combo for the first state (pre-search) and an input/search results component for the post search state and retain a single responsibility, just toggling between which component you want to render based on a simple state machine 1. open 2. search 3. filter 4. close But I think this example should meet all your requirements while being idiomatic to tanstack query and react principles. --- I'm off to bed, hope this helps šŸ™‚
wise-white
wise-whiteOP•2w ago
This was what I had researched as well they recommend using a separate state called has searched and store it as a Boolean and only run the query when has searched becomes true so by basically putting it in the enabled flag in the query With that I’d be able to get caching as well But at that time while I was coding I didn’t think of that and was put off by the idea of having an extra state so I went with using the refetch() method with removing queries on dialog close so the state is reset each time I open the dialog again
fair-rose
fair-rose•2w ago
Regardless, glad you got it working. It can take a while to paradigm shift into query if you are used to an existing alernate architecture
wise-white
wise-whiteOP•2w ago
Really appreciate you helping out ! It was tough to wrap my head around I guess you’re not in favor of the 1 input does all feature ? Also do u think having caching here is necessary for example searching the same search query again? Or does it not matter it’s just another query to fetch the same subreddits again for example with the searchterm Also say if I did want caching… when I close the dialog and open it again it would already pre fill with the last queries data right ? And that wouldn’t be ideal
fair-rose
fair-rose•2w ago
I don't mind the idea of having one input visually (I am no designer so can't comment on whether this is good or not, I'm sure it'll work out), but I do think the code will be significantly easier to understand if you just have two separate components, one for the first state and one for the second, and then just switch between which input you are rendering. I love Single Responsibility Principle, so whenever I see two states, I like to think of alternatives. I don't see why this wouldn't work though. If you have a key on your modal, the state for hasSearched will get reset whenever you close the modal, since react will essentially unmount your component. This means that when it is opened again, it will be a "new" component, and will therefore be back in the initial state. If you just use hasSearched as your check for whether or not to render the data, or what to do with the input in my above example, then you will still get caching, and can just conditionally use the data based on the hasSearched condition i.e.
{state.hasSearched && data?.map(...)}
{state.hasSearched && data?.map(...)}
Then you have cache and the expected functionality
wise-white
wise-whiteOP•2w ago
this is wonderful tbh! tysm! ill refactor my code when i get the time as now currently it works without the caching using refectch() and queryClient.removeQueries(querykey) ive also never used the key on these things apart from arrap mapping in react
fair-rose
fair-rose•2w ago
using a key is surprisingly uncommon, but it's the same reason that you put one on each array entry as you would use it in this case. When you add a key to a component, it let's react know how to associate it's internal states with the component. In an array, this is important because if you filter out some entries, there would be no way for react to know which entry maps to which state, so you add a key so that it knows which entry to associate. In this case, we can utilise the the key to do the opposite and make sure that react no longer thinks the component is the same component, since the key has changed. This causes it to reset all the state of your component, effectively the same as unmounting and remounting the component. Glad to help out, lemme know if you run into any other issues with this šŸ™‚
wise-white
wise-whiteOP•2w ago
DAMN i NEVER knew keys can be used in components like this!!! i always just used keys when i do array mapping and rendering jsx like this ! i learned something totally new! TYSM ill definitely look into refactoring my code very soon cuz right now it does work but theres no caching and im doing manual refetch() and removeQueries() so itd be nice to have cleaner code!
sensitive-blue
sensitive-blue•2w ago
Putting props to useState
Part deux of the useState pitfalls series is here, showing patterns to help solve the common use-case of initializing state with a prop.
wise-white
wise-whiteOP•2w ago
Jeeez i never came across keys apart from using them in lists to let react know what to update etc why didnt i ever learn all that while i was learning react šŸ™ tysm
sensitive-blue
sensitive-blue•2w ago
the new react docs are really good, I recommend reading them front-to-back, especially this page (which also shows the "key-trick"): https://react.dev/learn/you-might-not-need-an-effect
You Might Not Need an Effect – React
The library for web and native user interfaces
wise-white
wise-whiteOP•2w ago
ahhhh ive never ever really gone back to the react docs after learning it! i think i learnt it while it was on v18 etc when i used the docs back then they had the section on keys for using them while rendering JSX while mapping over an array to differentiate between the lists ! but that was it, i think they didnt have it in the docs back then! Ill definitely dive into this and read about it ! tysm guys

Did you find this page helpful?