How to prevent useQuery for running when state changes? - reactjs

I am using React Apollo to get data from my server. When my page loads I am using useQuery to retrieve the data. This works fine. The problem is when I make a change to the search form, this updates the state which causes an unwanted re-render which calls the server again.
I want to call the server only when the page loads and when I click the search button.
useQuery:
const { loading: loadingLessons, error: lessonsError, data: lessons } = useQuery(
LESSON_BY_PARAMS,
{
variables: { findLessonsInput: searchParams },
},
);
When I change a form field it calls updateFormField which updates the redux state causing a re-render
<Autocomplete
options={categories}
getOptionLabel={(option: any) => option.label}
inputValue={form.category}
defaultValue={() => {
const value = categories.find(option => option.label === form.category);
return value;
}}
onChange={(event, value) => updateFormField('category', value?.label)}
renderInput={params => (
<TextField {...params} label="Category" variant="outlined" fullWidth />
)}
/>
I am using react hooks.

Have a look at the skip option which can be used to entirely skip the query. You can do something like this:
const [skip, setSkip] = React.useState(false)
const { loading, data } = useQuery(QUERY, { skip })
React.useEffect(() => {
// check whether data exists
if (!loading && !!data) {
setSkip(true)
}
}, [data, loading])
So, once data returned you simply set skip option to true. If you want to make a request you should handle onClick on the search button(simply setSkip(false)).

If you use pure apollo client you will loose some features from apollo. There is no need to move graphql call into redux action, you can use selectors in Redux to prevent components from reloading or making unnecessary calls to a server.
Selectors in Redux

Thanks for the suggestion #Vadim Sheremetov however I decided to move the api call to a redux-thunk function:
When the page loads I dispatch an action passing it search params and apollo client:
const client = useApolloClient();
useEffect(() => {
loadLessons(searchParams, client);
}, [location.search]);
const mapDispatchToProps = dispatch => {
return {
loadLessons: (searchParams, client) =>
dispatch(loadLessonsAction(searchParams, client)),
};
};
The action invokes a redux thunk function which then dispatches another action that updates the state:
actions.ts:
export function loadLessonsAction(searchParams: any, client: ApolloClient<object>):
any {
return async function(dispatch) {
try {
const { data } = await client.query({
query: LESSON_BY_PARAMS,
variables: { findLessonsInput: searchParams },
});
dispatch(loadLessonsSuccessAction(data.lessonsByParams));
} catch (error) {}
};
}
export function loadLessonsSuccessAction(lessons: any[]): FilterPanelActionTypes {
return {
type: LOAD_LESSONS_SUCCESS,
payload: lessons,
};
}
reducer.ts:
case LOAD_LESSONS_SUCCESS: {
return {
...state,
lessons: action.payload.lessons,
lessonCount: action.payload.count,
};
}

Related

Pausing react query and re-fetching new data

I have a useQuery which is disabled in a react function component. I have another useQuery that uses mutate and on the success it calls refetchMovies(). This all seems to work well but I'm seeing old data in the refetchMovies. Is there a way for to get the refetchMovies to always call fresh data from the server when its called ?
const MyComponent = () => {
const {data, refetch: refetchMovies} = useQuery('movies', fetchMovies, {
query: {
enabled: false
}
})
const {mutate} = useQuery('list', fetchList)
const addList = useCallback(
(data) => {
mutate(
{
data: {
collection: data,
},
},
{
onSuccess: () => refetchMovies(),
onError: (error) => console.log('error')
}
)
},
[mutate, refetchMovies]
)
return (
<div onClick={addList}> {data} </div>
)
}
Try to invalidate the query in your onSuccess callback instead of manually refetching it:
https://tanstack.com/query/v4/docs/react/guides/query-invalidation
Example:
// Invalidate every query with a key that starts with `todos`
queryClient.invalidateQueries({ queryKey: ['todos'] })

Custom useAxios hook in react

I am using axios with react, so I thought to write a custom hook for this which I did and it is working fine like below
const useAxios = () => {
const [response, setResponse] = useState([]);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true); //different!
const [controller, setController] = useState();
const axiosFetch = async (configObj) => {
const { axiosInstance, method, url, requestConfig = {} } = configObj;
try {
const ctrl = new AbortController();
setController(ctrl);
const res = await axiosInstance[method.toLowerCase()](url, {
...requestConfig,
});
setResponse(res.data);
} catch (err) {
console.log(err.message);
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
console.log(controller);
// useEffect cleanup function
return () => controller && controller.abort();
}, [controller]);
return [response, error, loading, axiosFetch];
};
I have also created one axiosInstance to pass BASE_URL and headers.
Now calling useAxios to fetch data from api like below
const [data, error, loading, axiosFetch] = useAxios();
const getData = () => {
axiosFetch({
axiosInstance: axios,
method: "GET",
url: "/url",
});
};
useEffect(() => {
getData();
}, []);
My Question is
When I need to call one api I am doing above.
But what if I have to call three or four APIs in a single page.
Shall I replicate the code like this const [data1, error1, loading1, axiosFetch]=useAxios();
Or is there any other way to minimize the code.
Edit / Update
I ran above code to get data from /url, what if I want to hit different route to get one more data from server for other work, the base url remains the same
So if the second route is /users
const [data, error, loading, axiosFetch] = useAxios();
const getUsers = () => {
axiosFetch({
axiosInstance: axios,
method: "GET",
url: "/users",
});
};
useEffect(() => {
getUsers();
}, [on_BTN_Click]);
THe above codeI want to run in same file, one to get data and one to get users, how should I write my axios, as I think this const [data, error, loading, axiosFetch] = useAxios(); should gets called only once, Don't know how to do this or what is the correct way, shall I need to change my useAxios hook?
What you could do is pass the endpoint to the hook or properly call the axiosFetch callback with the different endpoints. But I have another opinion about what you are trying to do and here are my 5 cents on why this "axios hook" might not be a good idea.
A good rule of thumb on React Hooks is to use a custom hook if you need to encapsulate component logic that uses React Hooks.
Another important thing that is described in the React Hooks docs is:
Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.
So, eventually, if 2 different components call the fetch for the same endpoint, they both are going to execute the call to the Backend. How to prevent that? Well, you could use a lib such as React Query, that creates some kind of "cache" for you (and a bunch of other nice features!)
And last but not least: API calls are much more related to a Service/Module than a React Hook (isolate component logic), as a concept. I hardly advise you to create a service for making API calls and using that service inside your hook instead of coupling that logic to your hook and having to handle all kinds of issues such as Caching and multiple instances of the same hook or even multiple instances of this hook calling multiple different endpoints that eventually could or could not be dependant of themselves.
How about a generic useAsync hook that accepts any asynchronous call? This decouples axios specifics from the the hook.
function useAsync(func, deps = []) {
const [state, setState] = useState({ loading: true, error: null, data: null })
useEffect(
() => {
let mounted = true
func()
.then(data => mounted && setState({ loading: false, error: null, data }))
.catch(error => mounted && setState({ loading: false, error, data: null }))
return () => { mounted = false }
},
deps,
)
return state
}
Here's a basic example of its usage -
function UserProfile({ userId }) {
const user = useAsync(
() => axios.get(`/users/${userId}`), // async call
[userId], // dependencies
})
if (user.loading)
return <Loading />
if (user.error)
return <Error message={user.error.message} />
return <User user={user.data} />
}
The idea is any asynchronous operation can be performed. A more sophisticated example might look like this -
function UserProfile({ userId }) {
const profile = useAsync(
async () => {
const user = await axios.get(`/users/${userId}`)
const friends = await axios.get(`/users/${userId}/friends`)
const notifications = await axios.get(`/users/${userId}/notifications`)
return {user, friends, notifications}
},
[userId],
)
if (profile.loading) return <Loading />
if (profile.error) return <Error message={profile.error.message} />
return <>
<User user={profile.data.user} />
<Friends friends={profile.data.friends} />
<Notifications notifications={profile.data.notifications} />
</>
}
In the last example, all fetches need to complete before the data can begin rendering. You could use the useAsync hook multiple times to get parallel processing. Don't forget you have to check for loading and error before you can safely access data -
function UserProfile({ userId }) {
const user = useAsync(() => axios.get(`/users/${userId}`), [userId])
const friends = useAsync(() => axios.get(`/users/${userId}/friends`), [userId])
const notifications = useAsync(() => axios.get(`/users/${userId}/notifications`), [userId])
return <>
{ user.loading
? <Loading />
: user.error
? <Error message={user.error.message }
: <User user={user.data} />
}
{ friends.loading
? <Loading />
: friends.error
? <Error message={friends.error.message} />
: <Friends friends={friends.data} />
}
{ notifications.loading
? <Loading />
: notifications.error
? <Error message={notifications.error.message} />
: <Notifications notifications={notifications.data} />
}
</>
}
I would recommend you decouple axios from your components as well. You can do this by writing your own API module and even providing a useAPI hook. See this Q&A if that sounds interesting to you.

How to refetch/fetch after a series of mutation in graphql/apollo/react

Right now I have a use case to use two useMutations to create/update database. So the second one is depends on the success of the first one. And also the second mutation needs to be called in a loop, just think about that I have a array and I need loop through the array and apply the second mutation.
After all these mutation finished I have to refetch another api to update caches, because the cache would be impacted by the two mutations.
I am really struggling with how to achieve this.
From another post: Apollo Client - refetchQueries after multiple updates
I can do probably like
const [creatEnrollment] = useMutation(mut1)
const [updateEnrollment] = useMutation(mut2)
const [toFetch, {loading, error, data}] = useLazyQuery(UsersDocument)
await Promise.all([creatEnrollment(), updateEnrollment()])
const result = () => toFetch({
variables: {name: 'i'}
})
but the problem is 1. I need to execute second mutations after the first one; 2, I need to have an array that applied to second mutations one by one.
I also saw
here How can I wait for mutation execution in React Query?
we can use onSuccess
const mutate1 = useMutation((data) => axios.post('/something', { data }))
const mutate2 = useMutation(somethingResult) => axios.put('/somethingElse', { somethingResult })
<button onClick={() => {
mutate1.mutate('data', {
onSuccess: mutate2.mutate
})
}} />
But still 1. how to loop thru mutate2.mutate? and how to fetch after mutate2 finished
do like this????:
<button onClick={() => {
mutate1.mutate('data', {
onSuccess: mutate2.mutate
})
mutate2.mutate('data', {
onSuccess: query
})
}} />
Thank you for helping!!
You can have a function for useMutation and onSuccess the data which use get on success use other mutation
const mutationFuntion = (id) => {
// this is first mutation
return useMutation(
(newTitle) => axios
.patch(`/posts/${id}`, { title: newTitle })
.then(response => response.data),
{
// 💡 response of the mutation is passed to onSuccess
onSuccess: (data) => {
// call the api which will get all the latest update
},
}
)
}
Get the Data of first mutation
const [addTodo, { data, loading, error }] = mutationFuntion(//send data);
This is consecutive mutation I found it from this https://react-query-v3.tanstack.com/guides/mutations#consecutive-mutations doc
useMutation(addTodo, {
onSuccess: (data, error, variables, context) => {
// Will be called 3 times
},
})
['Todo 1', 'Todo 2', 'Todo 3'].forEach((todo) => {
mutate(todo, {
onSuccess: (data, error, variables, context) => {
// Will execute only once, for the last mutation (Todo 3),
// regardless which mutation resolves first
},
})
})
For handle the promise of every mutation call
const mutation = useMutation(addTodo)
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
Please you need to verify on what kind of object you want to call mutation in loop it array or some thing alse.

Multiple useLazyQuery hooks (Apollo Client) in React Function Component

I am trying to include two Apollo-Client useLazyQuery hooks in my function component. Either works fine alone with the other one commented out, but as soon as I include both, the second one does nothing. Any ideas?
export default function MainScreen(props) {
useEffect(() => {
validateWhenMounting();
}, []);
const [validateWhenMounting, { loading, error, data }] = useLazyQuery(
validateSessionToken,
{
onCompleted: (data) => console.log('data', data),
},
);
const [validate, { loading: loading2, error: error2, data: data2 }] =
useLazyQuery(validateSessionTokenWhenSending, {
onCompleted: (data2) => console.log('data2', data2),
});
const handleSendFirstMessage = (selectedCategory, title, messageText) => {
console.log(selectedCategory, title, messageText);
validate();
};
Figured it out: Adding the key-value pair fetchPolicy: 'network-only', after onCompleted does the trick. It seems that otherwise, no query is being conducted due to caching...
This is the pattern that I was talking about and mentioned in the comments:
const dummyComponent = () => {
const [lazyQuery] = useLazyQuery(DUMMY_QUERY, {variables: dummyVariable,
onCompleted: data => // -> some code here, you can also accept an state dispatch function here for manipulating some state outside
onError: error => // -> you can accept state dispatch function here to manipulate state from outside
});
return null;
}
this is also a pattern that you are going to need sometimes

React-Query and Query Invalidation Question

I don't really know how to ask clearly but, I will paste my code first and ask below.
function useToDos() {
const queryCache = useQueryCache();
const fetchTodos = useQuery(
'fetchTodos',
() => client.get(paths.todos).then(({ data }: any) => data),
{ enabled: false }
);
const createTodo = async ({ name ) =>
await client.post(paths.todos, { name }).then(({ data }) => data);
return {
fetchTodos,
createTodo: useMutation(createTodo, {
onMutate: newItem => {
queryCache.cancelQueries('fetchTodos');
const previousTodos = queryCache.getQueryData('fetchTodos');
queryCache.setQueryData('fetchTodos', old => [
...old,
newItem,
]);
return () => queryCache.setQueryData('fetchTodos', previousTodos);
},
}),
};
}
As you can see, I am trying to create my own custom hooks that wrap react-query functionality. Because of this, I need to set my fetchTodos query to be disabled so it doesn't run right away. However, does this break all background data fetching?
Specifically, when I run createTodo and the onMutate method triggers, I would ideally like to have the fetchTodos query update in the background so that my list of todos on the frontend is updated without having to make the request again. But it seems that with the query initially set to be disabled, the background updating doesn't take effect.
As I don't think wrapping react-query hooks into a library of custom hooks is a very great idea, I will probably have more questions about this same setup but for now, I will start here. Thank you. 😊
The mutation does not automatically triggers a refetch. The way to achieve this using react-query is via queryCache.invalidateQueries to invalidate the cache after the mutation. From the docs:
The invalidateQueries method can be used to invalidate and refetch single or multiple queries in the cache based on their query keys or any other functionally accessible property/state of the query. By default, all matching queries are immediately marked as stale and active queries are refetched in the background.
So you can configure the useMutation to invalidate the query when the mutation settles. Example:
function useToDos() {
const queryCache = useQueryCache();
const fetchTodos = useQuery(
'fetchTodos',
() => client.get(paths.todos).then(({ data }: any) => data),
{ enabled: false }
);
const createTodo = async ({ name ) =>
await client.post(paths.todos, { name }).then(({ data }) => data);
return {
fetchTodos,
createTodo: useMutation(createTodo, {
onMutate: newItem => {
queryCache.cancelQueries('fetchTodos');
const previousTodos = queryCache.getQueryData('fetchTodos');
queryCache.setQueryData('fetchTodos', old => [
...old,
newItem,
]);
return () => queryCache.setQueryData('fetchTodos', previousTodos);
},
onSettled: () => {
cache.invalidateQueries('fetchTodos');
}
}),
};
}
What about splitting the logic into two different hooks? Instead of a monolith like useToDos?
That way you could have a hook for fetching:
const fetchData = _ => client.get(paths.todos).then(({ data }: any) => data)
export default function useFetchTodo(
config = {
refetchOnWindowFocus: false,
enabled: false
}
) {
return useQuery('fetchData', fetchData, config)
}
And in your mutation hook you can refetch manually, before createTodo
import useFetchTodo from './useFetchTodo'
//
const { refetch } = useFetchTodo()
// before createTodo
refetch()

Resources