-
-
Notifications
You must be signed in to change notification settings - Fork 454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to manually (force) invalidate cache #297
Comments
So, we don't support this currently, but you have two options I believe. You could write your own cache exchange based on ours. This would probably be a simple copy and paste with an added method. For instance on the SSR exchange we have some special methods to extract or restore data. So you could add a method to reset the cache. The other option would be to wrap around the urql provider and completely recreate the client when the user logs out. I can maybe write up some code samples if you need some more concrete help on that 👍 |
Thanks for the prompt reply 🙌. I think the 2nd option feels a bit cleaner. I'll give it a go. Some code samples would be great. I imagine that this is quite a common scenario for a lot of apps with authentication. |
So if anyone else is interested in how I did this here's an example: Define a const makeClient = () =>
createClient({
url: process.env.REACT_APP_API_URL || '',
fetchOptions: () => {
const token = store.get('auth_token');
if (!token) {
return {};
}
return {
headers: {
authorization: `Bearer ${token}`,
},
};
},
}); Create a context that should "house" the const ClientContext = React.createContext<ClientState>({
// this is just to satisfy the TS compiler. If you're using JS you can just omit the default value.
resetClient: always(undefined),
});
function ClientProvider({ makeClient, children }: Props) {
const [client, setClient] = React.useState(makeClient());
return (
<ClientContext.Provider
value={{
resetClient: () => setClient(makeClient()),
}}
>
<UrqlProvider value={client}>{children}</UrqlProvider>
</ClientContext.Provider>
);
}
const useClient = () => React.useContext(ClientContext); The context itself exposes a Now, elsewhere in my app, anytime I want to flush the cache I can just get the const { resetClient } = useClient(); |
Glad you resolved this! I'll close this issue for now, since it's technically resolved sufficiently? But maybe this would be a great addition to our docs 👍 could be just a small FAQ entry explaining that resetting the client by recreating it is the easiest way to clear all of its state |
@kitten Is this still the recommended way of completely clearing/invalidating Urql's cache? My use case is on logout. I need the cache to be completely cleared on logout. I'm fine with the introspection query being resent if necessary, which the above solution will obviously do. Thank you for your time!...and btw, Urql rocks!! |
@Natas0007 Yep, that's still the safest approach to make sure everything's cleared 👍 also the introspection query is only sent in development when |
@kitten This seems to be working as expected, but it leads me to a quick follow up question. Since the devtools (Chrome) don't seem to update after recreating the client via resetClient()...and right click -> "Reload frame" on the devtools seems to completely clear the devtools until new queries are executed, what is the best way to verify the cache has been cleared? Do I need to write graphcache resolvers to get to the cache prop? Not a problem if so, and this will be a one-off test...I'm just curious as to the proper way of doing it. Thanks again for your time! |
@Natas0007 The cache will be cleared since it'll be recreated along with the client. That being said, creating a new client should definitely reset all data in the dev tools as well, so that may be worth an issue report over on the devtools repo since that sounds like a regression 😅 |
@kitten Thanks again! Issue created: urql-graphql/urql-devtools#325 |
About the original question? Any suggestion on how to do it when using Vue 3. Can I call provideClient again? It does not seem to be working... |
Hey @slacktracer, here's what I do. export const createUrqlClient = (logout: () => void) => {
return createClient(...);
}
export const client: Ref<Client | null> = ref(null); Some other file: export function logout() {
/*
This function should be called when logging out. It ensures that urql client is reset so that we have an empty cache.
*/
router.push('/login');
// other stuff you need to do as part of logging out
client.value = null;
} In watch(
() => client.value, // run whenever the ref defined earlier changes
() => {
console.log("providing new client")
// create and provide a new client is client.value is not set
if (!client.value) {
const newClient = createUrqlClient(logout);
client.value = newClient;
provideClient(newClient);
return;
}
// otherwise, provide the newly set client
provideClient(client.value);
}, { immediate: true }) Edit: I still have to check if this also creates a new websocket connection. |
@bartenra Thank you very much for your reply! Hopefully it will work for me just fine. 😁 👍 |
Just a heads-up that the method I describe below is no good. I've started a thread here: #1961 |
Had to modify the urql setup to recreate the client on logout to ensure that the cache gets completely cleaned up. Reference: urql-graphql/urql#297 (comment).
I have a similar situation except for I am using an offline cache that uses indexedDB. So adding the following tip for anyone who might want to reset the client that uses offline cache. On top of the suggested answer you want to use const storage = 'indexedDB' in window && makeDefaultStorage({ ..opts })
// ... code from the example from the above answer
...
resetClient: () => {
if (storage) {
storage.clear()
}
setClient(makeClient())
}
... |
did try the approach above by creating a new client but the devtool seems to stop working and had to reload the browser again, any ideas? |
@davidkhierl Fyi, the initial issue I created on the DevTools repo is still open (same issue you're experiencing): urql-graphql/urql-devtools#325 |
@Natas0007 Thank you for the response! If I understand correctly having two instance of client is the current work around? Just wondering how you end up with the issue, did you keep the same approach similar from what's suggested on the docs and just do a manual reload of the devtools? Thanks again! |
@davidkhierl No problem! It's not necessarily two client instances. The second is basically overwriting the first when resetClient is called. Since it kills dev tools (or at least did last time I checked), I initially verified this was working by doing some cache queries in my code. I only reset the client on logout, so once I verified it was working, it was a set it and forget it type deal. |
I can't seem to make my cache clear when a user signs out (on React). New components all mount with empty data. However, existing components do not re-render with cleared data. They still pull from the original cache. I've been doing this: // outside of my component:
const makeClient = (id?: string) => createClient(...)
// in my component:
const client = useMemo(() => makeClient(auth?.id), [auth?.id])
return <Provider value={client}>{...}</Provider> I expect this to completely clear out my cache when In case it helps, this is my Click to see codeconst makeClient = (uid?: string) => {
return createClient({
url,
exchanges: [
devtoolsExchange,
dedupExchange,
requestPolicyExchange({
ttl: 3000,
}),
cacheExchange<GraphCacheConfig>({
// https://formidable.com/open-source/urql/docs/graphcache/#installation-and-setup
keys: {
User: ({ firebaseId, id, ...user }) => {
if (firebaseId) {
return firebaseId
}
if (id) {
return id
}
console.error(
'[urql] client issue. User has no id. How did this happen?',
user
)
return null
},
UserDoesNotExist: () => null,
},
schema: schema as IntrospectionData,
resolvers: {
Query: {
me(parent, args, cache, info) {
// the "me" call is an inline fragment which can be ...on User or ...on UserDoesNotExist
const key: keyof User = 'firebaseId'
const typename: Pick<User, '__typename'>['__typename'] = 'User'
const fragment =
uid &&
cache.readFragment(gql`fragment _ on ${typename} { ${key} }`, {
[key]: uid,
})
if (fragment) {
return {
__typename: 'User',
[key]: uid,
}
}
return {
__typename: 'UserDoesNotExist',
}
},
},
},
}),
retryExchange({
// https://formidable.com/open-source/urql/docs/advanced/retry-operations/
}),
errorExchange({
onError(error) {
error.graphQLErrors.forEach(({ message, ...gqlError }) => {
Sentry.captureMessage(message, {
extra: { ...gqlError },
})
})
},
}),
authExchange<AuthState>({
async getAuth() {
const isSignedIn = getIsSignedIn()
if (!isSignedIn) {
return null
}
// TODO time this
const token = await getIdToken(false)
if (token) {
return {
token,
}
}
return null
},
willAuthError() {
// this gets called before the operation
// force the ID token to "fetch" every time
// since it's synchronous pre-expiration, that's fine.
return true
},
addAuthToOperation: ({ authState, operation }) => {
// the token isn't in the auth state, return the operation without changes
if (!authState?.token) {
return operation
}
// fetchOptions can be a function (See Client API) but you can simplify this based on usage
const fetchOptions =
typeof operation.context.fetchOptions === 'function'
? operation.context.fetchOptions()
: operation.context.fetchOptions
const headers = {
...fetchOptions?.headers,
Authorization: `Bearer ${authState.token}`,
}
return makeOperation(operation.kind, operation, {
...operation.context,
fetchOptions: {
...fetchOptions,
headers,
},
})
},
}),
fetchExchange,
],
})
} I tried setting the state instead, but no luck. Even though the instance is indeed resetting, the cache remains populated with previous requests. I tried this both in dev mode with dev tools and after running const { user } = useAuth()
const prevId = useRef<string>()
const [client, setClient] = useState(() => {
prevId.current = user?.uid
return makeClient(user?.uid)
})
useEffect(
function resetClientOnIdChange() {
if (prevId.current !== user?.uid) {
prevId.current = user?.uid
setClient(makeClient(user?.uid))
}
},
[user?.uid]
) I also logged const client = useClient()
const prevUrql = useRef(client)
useEffect(function onUrqlChange() {
if (client !== prevUrql.current) {
console.log('[urql] instance changed')
}
}, [client]) However, still no cache clearance. I opened an issue at #2511 |
For anyone else encountering this problem and needing to trigger an URQL client regeneration from a non-react component context within a React application - such as resetting an URQL client from Redux Sagas, I implemented the following. Essentially:
// The class factory
class urqlFactory {
private client: Client = this.generateClient();
private recreatedClientEventListeners: Record<
string,
(client: Client) => void
> = {};
public addRecreatedClientEventlistener(listener: (client: Client) => void) {
const newId = nanoid();
this.recreatedClientEventListeners[newId] = listener;
return newId;
}
public removeRecreatedClientEventListener(eventListenerId: string) {
delete this.recreatedClientEventListeners[eventListenerId];
}
public getClient(): Client {
return this.client;
}
public generateClient(): Client {
// this returns the value that graphql's createClient returns
const client = createURQLClient();
this.client = client;
return client;
}
public async regenerateClient(): Promise<Client> {
const context = URQLClientFactory;
return new Promise((resolve, _reject) => {
storage.clear();
const client = context.generateClient();
context.client = client;
Object.keys(context.recreatedClientEventListeners).forEach(
listenerKey => {
context.recreatedClientEventListeners[listenerKey](client);
},
);
resolve(client);
});
}
}
const URQLClientFactory = new urqlFactory();
export default URQLClientFactory; // root component
const Root = () => {
const [URQLClient, setURQLClient] = useState(URQLClientFactory.getClient());
/**
* when the URQL client factory recreates the URQL client, pass this to the
* URQL provider
*/
useEffect(() => {
const listenerID = URQLClientFactory.addRecreatedClientEventlistener(
newURQLClient => {
setURQLClient(newURQLClient);
},
);
return () => {
URQLClientFactory.removeRecreatedClientEventListener(listenerID);
};
}, []);
return (
<ReduxProvider store={store}>
<URQLProvider value={URQLClient}>
....
</URQLProvider>
</ReduxProvider>
)
}; Now in a redux saga we can recreate the URQL client: export function* requireUserSignOut(): SagaIterator {
yield call(URQLClientFactory.regenerateClient);
} |
I might be approaching this in the wrong way but I'm in the situation where, in my app, when a user logs out, I want to invalidate the entire cache.
Coming from Apollo I'd just call
client.resetStore()
. Urql's methodology of exchanges seems to work a bit differently and I can't see any such method on the client returned fromcreateClient
.Is this somthing I'd be able to do but manually composing the exchanges and hooking into the cache exchange or I am approaching this in completely the wrong way?
Thanks in advance
The text was updated successfully, but these errors were encountered: