Prevent Apollo from re-fetching if state changes in the Provider - reactjs

In our React application we store the currentUser in a state object that can change whenever the token is refreshed and is then passed in the header for API requests.
However because state changes in React cause the component tree to re-render it seems this also causes Apollo to re-fetch queries (even though these don't run on render but explicitly called via events (the events themselves are not re-called)).
So our Provider looks like this:
// ...rest of code removed for brevity...
// currentUser is a state object in a parent Context
const { currentUser } = useContext(AuthContext)
// ...rest of code removed for brevity...
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
authorization: currentUser
? `Bearer ${currentUser.authenticationToken}`
: ''
}
}
})
const client = useMemo(
() =>
new ApolloClient({
link: from([authLink, errorLink, httpLink]),
cache: new InMemoryCache({
dataIdFromObject: () => uuid.v4()
})
}),
[authLink, errorLink, httpLink]
)
return <ApolloProvider client={client}>{children}</ApolloProvider>
}
export default CustomApolloProvider
And then in some lower level component:
// This query will run after every currentUser change
// AFTER the first time this query is run via the click event
const [
loadData,
{ loading: loadingData, error: errorData }
] = useLazyQuery(DATA_QUERY, {
onCompleted: (data) => {
console.log('data', data)
}
})
<Button onClick={() => loadData()}>Load Data</Button>
So after clicking that Button to load some data, and then making the currentUser update, that query will then re-fetch... even though the query should ONLY be firing on event click...
Our current solution is to drop any use of 'states' inside this component and instead pull values directly from either a service or from localStorage for that header, so there's no chance of a re-fetch on the state change which works...
But this seems really flaky... is there a way to stop Apollo re-fetching these queries if a state changes inside its Provider?

Related

React-useForm : No formData are sent on the first request with react-query

As the title mention, I tried to combine react-query and react-useform.
but somehow, form data that are handled by use-form is empty when i tried to send them via api reques. I know there should be something wrong with my code since the data are perfecty sent on the next form submit.
here is my code :
const [formData, setFormData] = useState();
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm({
criteriaMode: 'all',
});
...
const { isFetching, isSuccess, isError, refetch } = useQuery(
'login',
() => userApi.login(formData),
{
onError: (res) => {
if (res.response.data) {
setErrorData(res.response.data.errors);
}
},
onSuccess: (res) => {
const userReqData = res.data.payload.user;
setUser(userReqData);
setErrorData({ errors: {} });
localStorage.setItem(
'access_token',
`Bearer ${res.data.payload.access_token}`
);
setTimeout(() => {
if (userReqData.level === 'admin' || userReqData === 'head_admin') {
navigate('/admin');
} else {
navigate('/me');
}
}, 2000);
},
enabled: false,
retry: false,
}
);
...
function handleLogin(data, e) {
// MAYBE THIS IS THE PROBLEM, formData sould be properly set first , but somehow it doesn't. The 'setFormData' works properly, but the 'formData' state is not updated on the first request(still empty, but not empty on console.log) . Instead, 'formData' is sent on the second request, which is strange.
setFormData(data);
refetch();
// or is there any other way to make refetch send the actual data from the handleLogin parameter right to the useQuery hook?
}
...
return (
<form
onSubmit={handleSubmit(handleLogin)}
>
...
</form>
)
'userApi' is an axios request that have been modifided with custom baseurl and headers, so, basicaly it's just a normal axios request.
library that i used :
react-query : https://react-query.tanstack.com/
https://react-hook-form.com/api/useform
You should use useQuery to fetch data, not to perform actions.
From the docs:
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 instead.
Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects
Here is a great resource that might help you refactor the code to a mutation.

refetch in reactQuery is not return the data

I am using reactQuery in my react application. I need to call one get API in button click. for that i am using refetch option in reactQuery. API call is working fine but my response data is coming undefined. I checked in browser network there i can see the response.
My API call using reactQuery
const { data: roles, refetch: roleRefetch } = useQuery('getRoles', () => api.getRoles('ID_234'), { enabled: false });
My click event
const handleAdd = (e) => { roleRefetch(); console.log(roles) }
My action call using axios
export const getRoles = (name) => axios.get(roles/list?sa_id=${name}, { headers: setHeader }).then(res => res);
const handleAdd = (e) => { roleRefetch(); console.log(roles) }
this not how react works, and it's not react-query specific. calling a function that updates some state will not have your state be available in the next line. It will make it available in the next render cycle. Conceptually, you want this to work, which cannot with how react is designed:
const [state, setState] = React.useState(0)
<button onClick={() => {
setState(1)
console.log(state)
}}
here, the log statement will log 0, not 1, because the update doesn't happen immediately, and this is totally expected.
With react-query, what you can do is await the refetch, because its async, and it will give you the result back:
const handleAdd = async (e) => {
const { data } = await roleRefetch();
console.log(data)
}
or, depending on what you actually want to do, you can:
use data in the render function to render something - it will always be up-to-date.
use theonSuccess callback of useQuery to trigger side-effects whenever data is fetched
spawn a useEffect in the render function that does the logging:
const { data: roles, refetch: roleRefetch } = useQuery('getRoles', () => api.getRoles('ID_234'), { enabled: false });
React.useEffect(() => {
console.log(roles)
}, [roles])
on a more general note, I think disabling a query and then calling refetch on a button click is very likely not idiomatic react-query. Usually, you have some local state that drives the query. in your case, that's likely the id. Dependencies of the query should go to the queryKey, and react-query will trigger a refetch automatically when the key changes. This will also give you caching by id. You can use enabled to defer querying when your dependencies are not yet ready. Here's what I would likely do:
const [id, setId] = React.useState(undefined)
const { data: roles } = useQuery(['getRoles', id], () => api.getRoles(id), { enabled: !!id });
const handleAdd = (e) => { setId('ID_234') }
of course, id doesn't have to come from local state - it could be some other form of client state as well, e.g. a more global one.

Redux toolkit RTK query mutation not getting returning data

Hi I recently learned the new react toolkit with the rtk query tool, and I am trying to put in a login system together using the createApi from the rtk package.
After giving it a test on the login button pressed, I see the network request going through without any issue(status code 200), and I get a response object providing user, token, however, when I try to get the returning data using useLoginMutation I get an undefined value.
below is the code for my endpoint which is injected in a base api:
export const apiLogin = theiaBaseApi.injectEndpoints({
endpoints: (build) => ({
loginUser: build.mutation<UserReadonly, loginValuesType | string>({
query: (values: loginValuesType, redirect?: string) => {
const { username, password } = values;
const header = gettingSomeHeaderHere
return {
url: "login",
method: "GET",
headers,
crossDomain: true,
responseType: "json",
};
},
}),
}),
});
export const { useLoginUserMutation } = apiLogin
then inside my React component I destructure the mutation result such like below:
const [login, {data, isLoading}] = useLoginUserMutation();
const submitLogin = () => {
// pass in username password from the form
login({username, password});
}
Suppose if I console log out data and isLoading I assume that I will see data: {user: "abc", token: "xyz"}, because under network tab of my inspect window I can see the response of this network request, but instead I am seeing data: undefined
Does any have experience on solving this?
Oh I found the reason, it was a very careless mistake. I had to wrap the reducer to my store, which was what I was missing
In my case the issue was that I was trying to access the UseMutationResult object inside onClick callback. And the object was not updating inside the callback, even though in the component the values were accurate.
If I put the log outside it's working just fine.
here is an example for better understanding (inside handleAddPost the mutationResult is not updating)
Here is a code sample (in case link is not working):
const Component = () => {
const [addPost, mutationResult] = useAddPostMutation();
...
const handleAddPost = async () => {
...
console.log("INSIDE CALLBACK isLoading and other data is not updating:");
console.log(JSON.parse(JSON.stringify(mutationResult)))
...
};
// in the example this is wrapped in an useEffect to limit the number of logs
console.log(mutationResult.data,"OUTSIDE CALLBACK isLoading and other data is working:")
console.log(JSON.parse(JSON.stringify(mutationResult)))
return (
...
<Button
...
onClick={handleAddPost}
>
Add Post
</Button>
...

How to fetch data in React blog app and stay DRY?

The question is simple. How to fetch data in your React blog and stay DRY? Let's say that you have just two components in your blog - PostsList and SinglePost, in both components you must fetch data, activate isLoading state, etc. There will be chunks of the same code in both components.
I investigated the situation a little bit, checking React-blog demo apps of big headless CMS providers, like Prismic or Sanity.io, and they all just repeat fetch functions in both PostsList and SinglePost.
Does anybody have any idea? You can point me to some good resources?
You can achieve this by using High Order Components. You can use them for reusing component logic. Let me show you an example of how to handle the isLoading with a HOC:
HOC:
import React, { useState } from 'react'
const hocLoading = (WrappedComponent, loadingMessage) => {
return props => {
const [ loading, setLoading ] = useState(true)
const setLoadingState = isComponentLoading => {
setLoading(isComponentLoading)
}
return(
<>
{loading && <p>{loadingMessage}</p>} //message showed when loading
<WrappedComponent {...props} setLoading={setLoadingState} />
</>
)
}
}
export default hocLoading
As you can see this HOC is receiving the WrappedComponent and a message that you can set depending on your component. Then you will have to wrap every component where you want to show the loading feedback with the HOC and you can use the setLoading prop to stop showing the loading feedback:
const Component = props => {
const { setLoading } = props
useEffect(() => {
const loadUsers = async () => {
await fetchData() // fetching data
setLoading(false) // this function comes from the HOC to set loading false
}
loadUsers()
},[ ])
return (
<div className="App">
{usuarios.data.map(x => <p key={x.id}>{x.title}</p>)}
</div>
);
}
export default hocLoading(Component, "Data is loading") //component wrapped
// with the HOC and setting feedback message
This way you avoid repeating this process for every component. Regarding the data fetching you can create a Hook or a function that receives dynamic params so you can just call something like fetchData(url). Here is an example of a dynamic function for making request using axios:
const baseUrl = "" //your BASE URL
async function request(url,method,data){
try {
const response = await axios({
method,
url: `${baseUrl}${url}`,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
data: data ? data : undefined
})
return response
} catch (e) {
// handle error
}
}

React: Apollo Client: Cannot update during an existing state transition

I am trying to make a component that wraps Apollo Client's Query component. I am using apollo-link-state for local state management and I want to have an error notification system that notifies the user of all the things.
my component looks like this...
export class Viewer extends React.Component {
static propTypes = {
children: PropTypes.func
};
render() {
const { children } = this.props;
return (
<Query query={GET_VIEWER}>
{({ data, client, error }) => {
if (error) {
client.mutate({
mutation: ADD_NOTIFICATION,
variables: { message: unpackApolloErr(error), type: 'Error' }
});
}
return children(data.viewer ? data.viewer : user);
}}
</Query>
);
}
}
but when it tries to add the error with the mutation, I get the react error..
Warning: forceUpdate(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.
I don't see an obvious way around this and I don't see why it is even happening if the client is provided as a render prop and cannot be used...I must be missing something simple but I cant see what it is
The answer turned out to be that the client is available in the onError function as this when using apollo-link-error https://www.apollographql.com/docs/link/links/error.html
I had tried to use onError before to access the client, but it was not obvious that the scope which it is called in does indeed contain the client even if the client is declared after the onError handler
// client is available here even though it is defined first
const err = onError({ graphQLErrors }) => {
this.client.mutate({ mutation: ADD_ERROR, variables: { graphQLErrors }})
}
const client = new ApolloClient({
cache,
link: ApolloLink.from([err, httpLink])
})
The reason why there was a Warning: forceUpdate was probably because Apollo internally will cause a forceUpdate when there is a mutation. So mutations should be in the render method.
The best way to handle mutations after an Apollo error is to add an onError link as described in https://www.apollographql.com/docs/react/features/error-handling.html#network
// Add errorLink into Apollo Client
const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
this.client.mutate({ mutation: ..., variables: {}});
});
const client = new ApolloClient({
cache,
link: ApolloLink.from([err, httpLink])
})

Resources