-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Race condition where data
or error
return 1
when key is null
#1345
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
Comments
I'm deep into debugging this, it's still hard for me to pinpoint. This is the case I'm finding currently (simplified): const key = () => userId || null
const swr = useSWR(key, fetcher) The However, this is the odd behavior: const key = () => userId || null
const swr = useSWR(key, fetcher)
const cachedData = cache.get(key)
console.log({
key: key(), // null
data: swr.data, // 1
error: swr.error // 1
cachedData // 1
}) I'm not entirely sure how this is possible. I think it occurs when I am signed in and then sign out. Basically, it seems that What's odd is, when I sign out, I call I'm going to set up a If this all ends up being my fault, apologies for the long issue. |
I have (sort of) a reproduction here. It looks like However, I'd have to peak into the code to know why, but my guess is that My first guess was, maybe I'm calling But my app doesn't use So what I'm wondering is: why, when my I think that SWR is calling To clarify: cache.set(null, 1)
console.log(cache.get(null)) // this logs 1 However: mutate(null, 1, false)
console.log(cache.get(null)) // this logs undefined Here is a full code example: import { useReducer } from "react";
import useSWR, { cache, mutate } from "swr";
const fetcher = (url) => fetch(url).then((res) => res.json());
// set the cache for key = null
cache.set(null, 1);
// try commenting the line above in favor of this:
// mutate(null, 1, false)
export default function App() {
const [active, toggle] = useReducer((s) => !s, true);
const { data, error } = useSWR(
!active ? null : "https://api.github.com/repos/vercel/swr",
fetcher
);
// when key is null
// this logs { error: 1, data: 1 }
console.log({ error, data, cache: cache.get(null) });
if (error) return <div onClick={toggle}>{error.message || "error"}</div>;
if (!data) return <div onClick={toggle}>Loading</div>;
return (
<div onClick={toggle}>
<h1>{data.name}</h1>
<p>{data.description}</p>
</div>
);
} |
Thanks for digging deep into this, @nandorojo! Since The fix is already in |
@shuding thanks so much for your reply! I do indeed use It's my understanding that |
@nandorojo the beta version doesn't have this requirement, you can still use |
SWR will serialize all the non-string keys internally before passing them to the custom cache provider. We are thinking about exposing that serialization util as APIs as well in #1337, but normally users shouldn't care about key serialization at all. |
Oh amazing, thanks for the clarification. I'll upgrade now then. Is there a type-safe way to match mutate array keys, then? Taking the example from the function matchMutate(matcher, data, shouldRevalidate = true) {
const keys = []
if (matcher instanceof RegExp) {
// `provider` is your cache implementation, for example a `Map()`
for (const k of provider.keys()) {
if (matcher.test(k)) {
keys.push(k)
}
}
} else {
keys.push(matcher)
}
const mutations = keys.map((k) => mutate(k, data, shouldRevalidate))
return Promise.all(mutations)
}
matchMutate(/^key-/) // revalidate keys starting with `key-`
matchMutate('key-a') // revalidate `key-a` |
I agree; I don't have any desire to do my own serialization. But it would be nice to mutate all keys where |
@shuding Question: can I still use the global |
We haven't looked into type-safe way of mutating specific cache keys, what's on the docs right now is mostly for showing ideas about how people might want to use the custom cache. That said, we definitely need to invest more time thinking about better APIs for revalidate keys by RegExp, or a matcher function. Regarding the global |
Got it! Is there a way to import the global cache without creating a custom one? I only use |
Currently the default global cache is not exposed because we are unsure about future changes (that's also a reason that we didn't document the old // swr-config.js
const m = new Map()
const { cache, mutate } = createCache(m)
export { cache, mutate, clear: m.clear } And use the config in your import { cache } from '../swr-config.js'
<SWRConfig value={{ cache }}>
...
</SWRConfig> When you need to mutate or clear the cache globally, you can import these inside any component: import { clear, mutate } from '../swr-config.js' |
I see. That's a reasonable approach to not documenting it too soon. I wanted to avoid the custom cache you mention because I use the global Plus, if I accidentally auto-import the global mutate from SWR instead of my own custom one, it could be hard to catch. Maybe I'll patch |
That's a very good point, we definitely want to find a way to avoid that. |
Maybe I'll make a named export to decrease odds of confusion: export const swr = { cache, mutate, clear: m.clear } So that I use But I'm so accustomed to using |
I upgraded to I'm using a custom diff --git a/node_modules/swr/dist/index.d.ts b/node_modules/swr/dist/index.d.ts
index dcbbce8..56f2b49 100644
--- a/node_modules/swr/dist/index.d.ts
+++ b/node_modules/swr/dist/index.d.ts
@@ -1,4 +1,4 @@
-export { SWRConfig, mutate, createCache } from './use-swr';
+export { SWRConfig, createCache } from './use-swr';
import useSWR from './use-swr';
export default useSWR;
export { SWRConfiguration, Revalidator, RevalidatorOptions, Key, KeyLoader, SWRResponse, Cache, SWRHook, Fetcher, MutatorCallback, Middleware } from './types'; |
@shuding Sorry to keep pinging you here, but the I reproduced on a sandbox here: https://codesandbox.io/s/xenodochial-forest-mr5qs?file=/pages/index.js Here is the result when you click "Clear Cache": |
Solved that issue by changing const swr = {
cache,
get clear() {
return map.clear;
}
}; Not sure why that fixed it 🤷🏼♂️ I refactored <button
onClick={() => {
map.clear();
render({});
}}
>
Clear Cache
</button> Doing this makes it sit loading for like 20 seconds though. Not sure why that is. Here's a video of the weird lag after clearing the cache: https://www.loom.com/share/c4664be033314a7d9f2dd3dbb0caf983?from_recorder=1&focus_title=1 I figured the behavior would be: Clear cache (synchronous) → UpdateI can confirm that there is a bug here with After I call https://www.loom.com/share/2a8351097eb74ce29d7728927554eb46 You can see in the |
Thanks @nandorojo! I updated the sandbox a bit to make it work: https://codesandbox.io/s/angry-snowflake-r6pj5?file=/pages/index.js. I think we have to mutate to force trigger revalidating for existing hooks. |
Got it, thanks @shuding! |
@shuding It seems like the solution you provided which mutates all the open options also doesn't exactly work for me. This is what's happening: await signOut()
await swr.clear() Since the
Is there a way to trigger revalidation for all those queries, but not actually refetch the network calls? All I want to do is clear the cache out, and that's it. UpdateDisregard this comment for now. Another updateNot adding a new comment to avoid spamming. I have been experiencing issues looping through each key and mutating. It causes revalidators to fire with the user ID, when the user's auth doesn't exist anymore. I'm trying to just keep The real problem is this: // the token is cleared
await signOut()
// however, not every component that accesses userId has re-rendered yet
// so now I call clear:
await cache.clear() Since I basically want to wait for Here's a visualization of what's happening // signOut() ----------> token = null -> keys.forEach(mutate)
// -> components re-render with userId = null
// ^ it re-renders after revalidations started
// this means mutate() is being called for stale keys
// it's called with token = null, userId != null |
The behavior of clearing cache is very hard to define. If you don't want to refetch (revalidate), what would you expect after clearing the cache? A re-render with all the data being empty? In that case you can call That's exactly what we are doing for signing out: await logout()
mutate(API_USER, null, false) |
Uh oh!
There was an error while loading. Please reload this page.
Bug report
Description / Observed Behavior
On very rare occasions, if I return
null
in the key function ofuseSWR
, mydata
and / orerror
field return1
.At first I thought it might be due to me calling
mutate
incorrectly somewhere. But it's happening for different calls (get user, get artists), so I no longer think that's the acse.Plus, if the key returns
null
, shouldn't thedata
anderror
always be undefined?I did manage to wrap these calls and logged that
error
was1
whilekey
wasnull
. But refreshing the page got rid of that bug.Expected Behavior
Don't return
1
fordata
orerror
if the key returnsnull
.Repro Steps / Code Example
I don't have a reproduction currently, I'm still working on that. I'm still trying to reproduce this in my own app.
So as it stands, I know that this issue might not be actionable. Hopefully this isn't a bug on the SWR side and I'll find it. However, I find it puzzling that SWR itself is returning these values.
Can any of the maintainers think of a potential reason this could be happening? Maybe an incorrect falsy check, or something like that, which results in SWR returning
1
?Additional Context
SWR version:
0.5.6
This started happening after I upgraded from
0.3.2
to0.5.6
, so I'm going to see if downgrading again solves it.The text was updated successfully, but these errors were encountered: