Skip to content
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

Closed
harrygr opened this issue Jun 13, 2019 · 20 comments
Closed

How to manually (force) invalidate cache #297

harrygr opened this issue Jun 13, 2019 · 20 comments

Comments

@harrygr
Copy link

harrygr commented Jun 13, 2019

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

@kitten
Copy link
Member

kitten commented Jun 13, 2019

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 👍

@harrygr
Copy link
Author

harrygr commented Jun 13, 2019

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.

@harrygr
Copy link
Author

harrygr commented Jun 23, 2019

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();

@kitten
Copy link
Member

kitten commented Jun 27, 2019

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

@Natas0007
Copy link

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

@kitten
Copy link
Member

kitten commented Dec 10, 2020

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

@Natas0007
Copy link

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

@kitten
Copy link
Member

kitten commented Dec 10, 2020

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

@Natas0007
Copy link

Natas0007 commented Dec 10, 2020

@kitten Thanks again!

Issue created: urql-graphql/urql-devtools#325

@slacktracer
Copy link

slacktracer commented May 24, 2021

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...
It creates a new websocket connection but seems to keep using a previously created one...

@bartenra
Copy link
Contributor

bartenra commented Jun 14, 2021

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.

@slacktracer
Copy link

@bartenra Thank you very much for your reply! Hopefully it will work for me just fine. 😁 👍

@bartenra
Copy link
Contributor

Just a heads-up that the method I describe below is no good.

I've started a thread here: #1961

randycoulman added a commit to randycoulman/freedom_account that referenced this issue Nov 27, 2021
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).
@adikari
Copy link

adikari commented Jan 12, 2022

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())
}
...

@davidkhierl
Copy link

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?

@Natas0007
Copy link

@davidkhierl Fyi, the initial issue I created on the DevTools repo is still open (same issue you're experiencing): urql-graphql/urql-devtools#325

@davidkhierl
Copy link

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

@Natas0007
Copy link

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

@nandorojo
Copy link
Contributor

nandorojo commented Jun 20, 2022

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

@kirgy
Copy link

kirgy commented Feb 1, 2023

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:

  1. Use a class to store the URQL client within state on the class
  2. export that instantiated class (the object, not the class)
  3. create event listeners on the class to enable other contexts to register callbacks when the URQL client is regenerated
  4. in the root component:
    1. register an event listener on mount
    2. store the URQL client from the class object in state
    3. on callback from a regeneration, update the state
// 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);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants