urql: How to manually (force) invalidate cache

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 from createClient.

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

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 9
  • Comments: 20 (7 by maintainers)

Commits related to this issue

Most upvoted comments

So if anyone else is interested in how I did this here’s an example:

Define a makeClient function. Mine looks like this.

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 client. One of its props should be the makeClient function. You could just use the createClient function directly imported from URQL but this approach allows dependency injection so different clients can be used depending on the situation, as well as any app-specific configuration can be placed outside of this component.

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 resetClient function that simply creates a new client and updates its state with it. The URQL provider is passed it as a prop.

Now, elsewhere in my app, anytime I want to flush the cache I can just get the resetClient function from useClient and call it. URQL then refetches any queries that’re needed:

const { resetClient } = useClient();

Hey @slacktracer, here’s what I do. In ./setup/urql.ts:

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 App.vue, setup()

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.

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 storage.clear().

const storage = 'indexedDB' in window && makeDefaultStorage({ ..opts })

// ... code from the example from the above answer

...
resetClient: () => {
  if (storage) {
    storage.clear()
  }
  setClient(makeClient())
}
...

@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!!

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 auth.id changes. However, it seems like, after signing out, the cache is getting retained. The provider is at the root of my app, and I only have one, so I’m a bit confused as to how that could be happening. Does anything come to mind?

In case it helps, this is my makeClient function:

Click to see code
const 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 next build.

  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 useClient’s result in my consuming components, and it is indeed a different instance:

  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

@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.

@davidkhierl Fyi, the initial issue I created on the DevTools repo is still open (same issue you’re experiencing): https://github.com/FormidableLabs/urql-devtools/issues/325

@Natas0007 Yep, that’s still the safest approach to make sure everything’s cleared 👍 also the introspection query is only sent in development when @urql/devtools is used.

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