Apollo useQuery() - "refetch" is ignored if the response is the same - graphql

I am trying to use Apollo-client to pull my users info and stuck with this problem:
I have this Container component responsible for pulling the user's data (not authentication) once it is rendered. User may be logged in or not, the query returns either viewer = null or viewer = {...usersProps}.
Container makes the request const { data, refetch } = useQuery<Viewer>(VIEWER);, successfully receives the response and saves it in the data property that I use to read .viewer from and set it as my current user.
Then the user can log-out, once they do that I clear the Container's user property setUser(undefined) (not showed in the code below, not important).
The problem occurred when I try to re-login: Call of refetch triggers the graphql http request but since it returns the same data that was returned during the previous initial login - useQuery() ignores it and does not update data. Well, technically there could not be an update, the data is the same. So my code setUser(viewer); does not getting executed for second time and user stucks on the login page.
const { data, refetch } = useQuery<Viewer>(VIEWER);
const viewer = data && data.viewer;
useEffect(() => {
if (viewer) {
setUser(viewer);
}
}, [ viewer ]);
That query with the same response ignore almost makes sense, so I tried different approach, with callbacks:
const { refetch } = useQuery<Viewer>(VIEWER, {
onCompleted: data => {
if (data.viewer) {
setUser(data.viewer);
}
}
});
Here I would totally expect Apollo to call the onCompleted callback, with the same data or not... but it does not do that. So I am kinda stuck with this - how do I make Apollo to react on my query's refetch so I could re-populate user in my Container's state?

This is a scenario where apollo's caches come handy.
Client
import { resolvers, typeDefs } from './resolvers';
let cache = new InMemoryCache()
const client = new ApolloClient({
cache,
link: new HttpLink({
uri: 'http://localhost:4000/graphql',
headers: {
authorization: localStorage.getItem('token'),
},
}),
typeDefs,
resolvers,
});
cache.writeData({
data: {
isLoggedIn: !!localStorage.getItem('token'),
cartItems: [],
},
})
LoginPage
const IS_LOGGED_IN = gql`
query IsUserLoggedIn {
isLoggedIn #client
}
`;
function IsLoggedIn() {
const { data } = useQuery(IS_LOGGED_IN);
return data.isLoggedIn ? <Pages /> : <Login />;
}
onLogin
function Login() {
const { data, refetch } = useQuery(LOGIN_QUERY);
let viewer = data && data.viewer
if (viewer){
localStorage.setItem('token',viewer.token)
}
// rest of the stuff
}
onLogout
onLogout={() => {
client.writeData({ data: { isLoggedIn: false } });
localStorage.clear();
}}
For more information regarding management of local state. Check this out.
Hope this helps!

Related

Query on page not refetching once navigating back to page

In my nextjs page I have the following hook (generated by using graphql-codegen) that fetches a graphql query.
const { data, error, loading, fetchMore, refetch, variables } = useGetShortlistQuery({
notifyOnNetworkStatusChange: true, // updates loading value
defaultOptions: {
variables: {
offset: undefined,
filterBy: undefined,
sortBy: SortBy.RecentlyAdded,
sortDirection: SortDirection.Desc,
},
},
});
This is the useGetShortlistQuery hook that is generated by graphql-codegen
export function useGetShortlistQuery(
baseOptions?: Apollo.QueryHookOptions<GetShortlistQuery, GetShortlistQueryVariables>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<GetShortlistQuery, GetShortlistQueryVariables>(GetShortlistDocument, options);
}
my component is wrapped in a HOC to enable Apollo Client
export default withApollo({ ssr: true })(Index);
The withApollo HOC uses #apollo/client and the cache property of the apollo client is as follows.
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
getShortlist: {
keyArgs: [],
merge(existing: PaginatedProperties | undefined, incoming: PaginatedProperties): PaginatedProperties {
return {
...incoming,
properties: [...(existing?.properties || []), ...(incoming?.properties || [])],
};
},
},
},
},
},
}),
The problem I am having is that on this page I update the variables on the useGetShortlistQuery using refetch which, in turn, updates the data.
However, if I navigate to another page, then come back to this page using this component. It doesn't seem to retrigger the graphql query so returns the previous data.
If you are using getStaticProps (or getServerSideProps) with pre rendered pages, it is a known behavior. It is due to optimisation by Next.js not re-rendering components between page navigations, with pages like [id].js.
The trick is to have a key on components that you want to see refreshing. You have multiple ways to do so. Having a different key on components tells React that it should be re-rendering the components, and thus will trigger again the hooks.
Practical example:
export const getStaticProps: GetStaticProps = async ({ params }) => {
const data = getData() //something that fetches your data here
return {
props: {
// some other data you want to return...
// some unique value that will be on each page
key: data.key
},
}
}
const MyPage: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = (props) => {
<div key={props.key} />
}

Update the cache of Apollo client 3 when polling not working

I am playing with the cache of #apollo/client v3. Here's the codesandbox.
I am adding a user to a cached list of users using client.writeQuery, and the query has a pollInterval to refetch every few seconds.
I am able to add the user to the list, it does refresh the UI, and I can see the pollInterval working in the network tab of Chrome.
THE PROBLEM
I would expect the list of users to return to its initial state when the polling kicks in, and overwrite the user I added manually to the cache, but it does not.
Apollo config
export const cache = new InMemoryCache();
const client = new ApolloClient({
cache,
link: new HttpLink({
uri: "https://fakeql.com/graphql/218375d695835e0850a14a3c505a6447"
})
});
UserList
export const UserList = () => {
const { optimisticAddUserToCache, data, loading } = useUserList();
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<button onClick={() => optimisticAddUserToCache()}>Add User to cache</button>
<ol>
{data?.users.map(user => {
return <li key={user.id}>{user.firstname}</li>;
})}
</ol>
</div>
);
}
useUserList
const GET_USER_LIST = gql`
query Users {
users {
id
firstname
}
}
`;
export const useUserList = () => {
const { loading, error, data, refetch } = useQuery(GET_USER_LIST, {
pollInterval: 4000 // It does poll (check chromes's network tab), but it doesn't seem to overwrite the cache
});
const client = useApolloClient();
const optimisticAddUserToCache = () => {
const newUser: any = {
id: `userId-${Math.random()}`,
firstname: "JOHN DOE",
__typename: "User"
};
const currentUserList = client.readQuery({ query: GET_USER_LIST }).users;
// This works, it does add a user, and UI refreshes.
client.writeQuery({
query: GET_USER_LIST,
data: {
users: [newUser, ...currentUserList]
}
});
};
return { optimisticAddUserToCache, loading, error, data, refetch };
};
Working as expected (almost)
Polled response arrives always with the same data ...
... doesn't result in write to cache (no content compared) ...
... no data change in cache ...
... data property (from useQuery) not updated ...
... no data updated, no component rerendering.
For optimistic update working you need a real mutation, real [persisted] change on remote datasource ... propagated to next polled responses.

How can I use Apollo's cacheRedirect with a nested query

I've got a query that looks like this:
export const GET_PROJECT = gql`
query GetProject($id: String!) {
homework {
getProject(id: $id) {
...ProjectFields
}
}
}
${ProjectFieldsFragment}
`;
My InMemoryCache looks like this:
const cache = new InMemoryCache({
dataIdFromObject: ({ id }) => id,
cacheRedirects: {
Query: {
getProject: (_, args, obj) => {
console.log('Hello world');
},
},
}
});
The above cache redirect is never hit. However, if I modify it to look like:
const cache = new InMemoryCache({
dataIdFromObject: ({ id }) => id,
cacheRedirects: {
Query: {
homework: (_, args, obj) => {
console.log('Hello world');
},
},
}
});
It does get hit, however I don't have any of the arguments that are passed in the nested getProject query. What's also confusing is that this cache redirect function is hit for queries that it seemingly shouldn't get hit for, like:
export const SESSION = gql`
query Session {
session {
user {
id
fullName
email
}
organizations {
name
id
}
}
}
`;
So what is going on? I've resorted to just using readFragment in the places where I want the cache to redirect, but I'd like for that logic to become centralized.
It's hard to say for sure with these kinds of issues, but I'm betting that, since you say
What's also confusing is that this cache redirect function is hit for queries that it seemingly shouldn't get hit for
the issue might be with your dataIdFromObject function.
This function is ultimately what decides if data is read from the cache or not. You should only override this if you have a very specific reason to. For example:
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory';
// ...
export default new ApolloClient({
link,
cache: new InMemoryCache({
dataIdFromObject(object) {
switch (object.__typename) {
case 'ModifierScale':
case 'ModifierGroup':
return [
object.__typename,
object.id,
...object.defaults
.map((defaultModifier) => defaultModifier.id)
.join(''),
].join('');
default:
return defaultDataIdFromObject(object); // fall back to default handling
}
},
}),
});
The point of this setting is to allow you to customize the key that gets put into the cache when you are loading the data.
If this doesn't solve your issue, I would definitely go into the Apollo tab in the chrome dev tools (you need the Apollo dev tools chrome extension to do this) and look at the cache section. It should show you the data in the cache and the key that the data is stored in.

Apollo client not sending token to backend until page refresh

I've been working on an app and only realized this issue when I started to clear the cache, but my app only works fine on refresh. When I clear all the cache, refresh then run through my app, I realized that my queries were returning my custom error "GraphQL error: Not authenticated as user".
I believe something is wrong with the way that I've set up my apollo client. It seems that the context is being set as soon as it's instantiated and then never changes the context even if the token exists. It would also explain why after logging in then refreshing, the queries work with the token until the local storage/cache is cleared. So my question is what's wrong with what I have?
import { persistCache } from "apollo-cache-persist";
import { ISLOGGEDIN_QUERY } from "./components/gql/Queries"
const cache = new InMemoryCache();
const token = localStorage.getItem('token')
persistCache({
cache,
storage: localStorage
})
const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
cache,
resolvers: {
Mutation: {
changeValue: (_, args, { cache }) => {
const { isAuth } = token ? cache.readQuery({ query: ISLOGGEDIN_QUERY }) : false;
cache.writeData({
data: { isAuth: !isAuth }
})
return null;
}
}
},
request: (operation) => {
operation.setContext({
headers: {
authorization: token ? token : ''
}
})
},
});
//set default values
client.cache.writeData({ data: { isAuth: token ? true : false } })
export default client;```
I know I'm a bit late but I was having this problem too and found these
https://www.apollographql.com/docs/react/networking/authentication/#reset-store-on-logout
https://stackoverflow.com/a/65204972/13491532
You can just call clear store after your login mutation
import { useApolloClient } from "#apollo/client";
const client = useApolloClient();
client.clearStore();

apollo client 2.0 graphql react auth

I'm new to apollo/graphql and I'm trying to get my authentication done properly in a greenfield project. My authentication provider is AWS cognito. I wrote a cognito helper module to interact with it.
Though I'm not quite sure how to sync my apollo client with my auth state.
export const authenticate = (username: string, password: string) => {
const authDetails = new AuthenticationDetails({
Username: username,
Password: password,
})
const cognitoUser = getCognitoUser(username)
return new Promise((resolve, reject) => {
cognitoUser.authenticateUser(authDetails, {
onSuccess: result => {
resolve(result)
},
onFailure: err => {
reject(err)
},
})
})
}
export const getCurrentUserToken = () => {
return new Promise((resolve, reject) => {
const currentUser = userPool.getCurrentUser()
if (currentUser) {
currentUser.getSession((error, session) => {
if (error) {
reject(error)
}
resolve(session.getIdToken().getJwtToken())
})
} else {
resolve(null)
}
})
}
export const logout = () => {
const currentUser = userPool.getCurrentUser()
if (currentUser) {
currentUser.signOut()
}
}
Right now I'm just using these function to handle my login by calling them in my react component handlers. I configured an apollo-link for adding the auth header. Inject my JWT token data into context at the backend and implemented a currentUser query resolver in the backend.
const resolvers = {
RootQuery: {
currentUser: (obj, args, context) =>
context.tokenData
? {
id: context.tokenData.sub,
name: context.tokenData.name,
email: context.tokenData.email,
username: context.tokenData['cognito:username'],
}
: null,
},
}
In my react App layout i got a component UserPanel which queries that currentUser query.
const CURRENT_USER_QUERY = gql`
query {
currentUser {
name
}
}
`
export default graphql(CURRENT_USER_QUERY)(UserPanel)
When i am logging in now obviously the UserPanel does not update its currentUser query except I'm reloading the page ofc. Though im also having troubles finding a good solution to sync them.
I was thinking about implementing my login via graphql mutation using apollo-link-state to do it locally and watch these to refetch if someone logged in/out. I'm not sure if this is fine since it seems to me that this link cannot resolve async stuff (e.g. promises) in its mutation resolvers.
Another option I was thinking about was to decouple the auth process from the apollo client completely and implement some auth pubsub system maybe with Observables and let the react components refetch the queries if the authentication state changes.
I'm very uncertain how to continue and every solution I'm thinking about doesn't feel like the recommended way to go.
I don't have the full picture with regards to your React setup but here I go. It might be that Apollo-client is caching CURRENT_USER_QUERY locally and is showing you the results of a previous query. You could try the network-only option on the query:
export default graphql(CURRENT_USER_QUERY, { options: {fetchPolicy: 'network-only' }})(UserPanel)
What I have in React is an AppContainer which is my parent component. It checks if the user is logged in:
const loggedInUser = gql`
query loggedInUser{
user {
id
role
}
}`
export default graphql(loggedInUser, { options: {fetchPolicy: 'network-only' }})(AppContainer)
Then on my UserProfile page, I use a data container to fetch the data before passing it down to the UserProfile child component. I think the loggedInUser query automatically updates the user in the apollo store. With it apollo-client realizes that it needs to refetch userQuery. Does that help?
const userQuery = gql`
query userQuery {
user {
id
name
email
role
company
}
}
`
export default graphql(userQuery, {name: 'userQuery'})(UserDataContainer);

Resources