We were unable to load Disqus. If you are a moderator please see our troubleshooting guide.

Santiago Oquendo • 10 months ago

New fear unlocked :P. Thanks for the fantastic post!

Kevin • 10 months ago

Thank you.

:) At least it's easy enough to fix and usually not a big issue if the components are small enough or only handle a couple of strings here and there. The reason why it tripped us up at Ramblr.ai was that we handle large arrays of image data.

Still good to be aware!

Mohamed Hussain • 10 months ago

Thanks for sharing this with he community.

I read this on mobile, I didn't check on PC.

My doubt is even if we use custom hook that is still having the parent scope as app.scope which has bigdata.

How this is solved using custom hook?

Sorry for if I understood something wrong here.

More on how custom hook solves this issue helps me

Kevin • 10 months ago

The essential part is that you have to define the custom hook outside the App function, i.e., in the module scope. This way the App closure is not being added to the scope chain for your queryFn. The queryFn might still close over other variables that are present in your custom hook's function, but those should be fewer and not 10MB.

Михаил Дронов • 10 months ago

Or you could isolate query function

const queryFN=async ({queryKey}) => { return fetchPost(String(queryKey[1]));}

...

const { data, error, isPending } = useQuery({ queryKey: ['posts', id], queryFn: queryFN, });

If defined outside of App scope - should work without leaking memory

Kevin • 10 months ago

Actually TKDodo from the ReactQuery team suggested something similar, using their new queryOptions API.
Check out: https://x.com/TkDodo/status...

Very interesting, thanks for sharing. I'm guessing an abstraction with `queryOptions` instead of a custom hook would also work? Like:

import { queryOptions} from '@​tanstack/react-query'const postOptions = (id) => queryOptions({
queryKey: ["posts", id],
queryFn: () => fetchPosts(id)
})


then, in the component:

const { data} = useQuery(postOptions(id))
Fernando Rojo • 10 months ago

These two pieces on closures were great. Had no idea about the memory risks, but it makes total sense. Thanks for sharing.

anilanar • 10 months ago

Closures don’t capture everything from the parent scope, they only capture things that are referenced in the current closure. You should double check your experiment.

João Paulo Abadio Grimaldi • 10 months ago

Its pretty simple, if you can have a react code that looks like this

function MyComponent() {
const bigObject = new BigObject();
const myCallback = () => console.log(bigObject);
}

it means you can access bigObject from myCallback. For that, it needs to exist in a shared space (that is the context object), even if you use useCallback, the object still can be referenced and it will still exist in the context, it will only not trigger re-renders.

AFAIK, react doesnt check if a variable is called or not on a function to share it.

anilanar • 10 months ago

You are referencing the object because you have a console.log. If you don’t reference the variable at all, it doesn’t cause bigObject to be retained in memory even if, let’s say, you assign myCallback to window.myCallback. Garbage collector will eventually clean up bigObject once MyComponent unmounts.

Kevin • 10 months ago

If you never reference bigObject anywhere it will get GCd, that’s right. If it appears in any of the closures it will be retained as long as the last closure is being referenced.
Just run the code, force GC and snapshot the JS heap. Yes, this is not React or React Query, but the way closures are implemented.
JS is not full of leaks since we rarely combine multiple parallel closures from the same function, big objects and long lived closure references. So we just don’t notice this much.
Same issue: http://point.davidglasser.n... Read the first couple of paragraphs: https://mrale.ph/blog/2012/... Quote from link:

Another thing to keep in mind: if Context is potentially needed it will be created eagerly when you enter the scope and will be shared by all closures created in the scope. If scope itself is nested inside a closure then newly created Context will have a pointer to the parent. This might lead to surprising memory leaks.
anilanar • 10 months ago

It’s not React that does it. It’s JS runtime optimization. You can prove it by trying to inspect variables from a parent scope in a closure that is called asynchronously.

If that was really a memory leak, everything in JS would be riddled by memory leaks. This post is dangerously misleading because it was part of a popular JS newsletter and appears quite high in Google searches.

Kevin • 10 months ago

I can very much access bigObject after App() has finished. Just run the debugger. See screenshot below:
https://uploads.disquscdn.c...

Kevin • 10 months ago

The closure scope / context object that holds on to the captured variables gets created when App() is called and the inner functions/expressions are being created.

Now the important part, this context object is shared by all the inner functions.
We have three in the first example:

1. function handleClick() { ... }
2. the queryFn
3. () => setId((p) => p + 1)

The union of these closes over id, setId, and unfortunately bigObject. See the screenshot for a breakpoint in handleClick() - it only accesses bigObject, but also captures id and setId.

https://uploads.disquscdn.c...

anilanar • 10 months ago
evan • 10 months ago

🤔️
"As soon as you define the queryFn within the App function, it will capture the App scope and keep the bigData object in memory" ?

kal • 10 months ago

did you try to run into these issue in pure javascript fct to see if the pb is them or juste how javascript interpretter work
Because I tried both example and I had the same ussie when ussing react without usecallback and react query

Kevin • 10 months ago

Yes, it’s a JS thing. Just common in idiomatic React(Query) to use a lot of closures.

Marlen • 10 months ago

I can't seem to get my head around this. Isn't the handleClick function recreated on every render of the App component? If so, isn't also its closure recreated?

Kevin • 10 months ago

You’re right, handleClick is recreated, but the querFn is kept in the cache. The queryFn holds on to bigObject, not handleClick. handleClick just puts it on the closure context when App() is called.