React useMemo memory clean
Asked Answered
P

3

8

How i can do memory cleaning by cat.destroy() method when component is dead?

const object = useMemo(() => {
  return new Cat()
}, [])
Psalmbook answered 2/3, 2021 at 20:27 Comment(0)
O
2

clean up effects for hooks you execute when a component will be unmounted. It's performed by a returned function at your useEffect hook:

useEffect(() => {
  // return a function to be executed at component unmount
  return () => object.destroy()
}, [object])

Note: as @Guillaume Racicot pointed out, in some cases it's possible the object not being created yet by the time unmount is executed, hence you would face an error. In this case remember to conditionally executing destroy, you could use optional chaining object?.destroy() for that.

Overvalue answered 2/3, 2021 at 20:40 Comment(10)
That's not an answer to the question. The question was: how to clean up something while using memoizationSeiber
@Seiber your comment doesn't make much sense. to execute a clean up effect on dismount (dead component like OP asked) you should return a function with the proper expected action (cat.destroy() in this case). the fact that the object is memoized or not is irrelevantOvervalue
This won't work in react 18Vaish
@GuillaumeRacicot why is that? useEffect signature didn't change in React 18.Overvalue
@Overvalue effects are run twice on mount in react 18, so this destroy will be called on mount before the second effect run.Vaish
@GuillaumeRacicot effects don't run twice in react 18, the React.StrictMode functionality forces that. React.StrictMode works only in development environment and not in production. And its usage is also optional. I suggest you to read more about in the React docsOvervalue
@Overvalue Strict mode does that only because it would cause bugs when an effect is misbehaving. So the strict mode actually flag this effect as being broken by calling object.destroy() on the first render.Vaish
that sounds reasonable, I just edited it.Overvalue
useEffect has quite a different lifecycle to useMemo or useRef. Let's quote the React documentation: "Your cleanup logic should be “symmetrical” to the setup logic ... If you have cleanup code without corresponding setup code, it’s usually a code smell". This answer falls right into the trap.Barrie
I think what @Barrie is getting at above is that useMemo is intended for caching purposes only. It's mentioned in the caveats section of the docs. React could, e.g., throw away the cached value for various reasons in the future. This could mean you could get two new Cat() calls with only one cleanup call from a useEffect cleanup function. Guaranteeing equal cleanups to memo calls would require more complexity.Silma
S
5

So, I literally thought for sure @buzatto's answer was correct. But having read the discussion, I tried some tests myself, and see the major difference.

It's been a couple years, so it's possible @buzatto's answer may have worked better then, but as of 2023, do not use @buzatto's answer. In certain scenarios it will clean up right after setting up. Read below to understand why.

  1. If you're here to understand the difference, follow below.

  2. TL;DR: If you're here because you want ACTUALLY working useMemo cleanup, jump to the bottom.

Lifecycle comparison: useMemo vs. useEffect

If you render the following component in React StrictMode:

function Test() {
    const id = useId();

    const data = useMemo(() => {
        console.log('memo for ' + id);
        return null;
    }, []);

    useEffect(() => {
        console.log('effect for ' + id);

        return () => {
            console.log('effect clean up ' + id);
        }
    }, []);

    return (
        <div>Test</div>
    )
}

You may expect to get these results:

// NOT actual results
memo for :r0:
effect for :r0:
effect clean up for :r0:
memo for :r1:
effect for :r1:
effect clean up for :r1:

But to our surprise, we actually get:

// ACTUAL results
memo for :r0:
memo for :r1:
effect for :r1:
effect clean up :r1:
effect for :r1:

As you can see, the effect never even ran for the first iteration of the component. This is because useEffect is a render side-effect, and the first version of the component never actually rendered.

Also note how React's strict mode double-running operates: It runs the useMemo twice, once for each version of the component, but then runs the useEffect twice as well, both for the second component.

memo for :r0: // sets up Cat[0]
memo for :r1: // sets up Cat[1]
effect for :r1:
effect clean up :r1: // cleans up Cat[1] NOT Cat[0]]
effect for :r1:

This is why the memo will be cleaned up right after it's set up.

 


 

TL;DR: A useMemoCleanup Hook

Ok, so because we cannot rely on effects at all (since they may never run for a certain version of a component), and until React provides a hook that does allow cleaning up a component that never rendered, we must rely on JS.

Fortunately, modern browsers support a feature called FinalizationRegistry. With a FinalizationRegistry, we can register a value. Then, when that value is garbage-collected by the browser, a callback will get triggered and passed a 'handled' value (in our case, the cleanup method).

Using this, and the fact that React refs and the useRef hook do follow the same lifecycle as useMemo, the following code can be used to allow cleanup from within a useMemo.

Usage: Return [returnValue, CleanupCallback] from your callback:

import { useMemo, useRef } from "react";

const registry = new FinalizationRegistry(cleanupRef => {
    cleanupRef.current && cleanupRef.current(); // cleanup on unmount
});

/** useMemoCleanup
 * A version of useMemo that allows cleanup.
 * Return a tuple from the callback: [returnValue, cleanupFunction]
 * */
export default function useMemoCleanup(callback, deps) {
    const cleanupRef = useRef(null); // holds a cleanup value
    const unmountRef = useRef(false); // the GC-triggering candidate

    if(!unmountRef.current) {
        unmountRef.current = true;
        // this works since refs are preserved for the component's lifetime
        registry.register(unmountRef, cleanupRef);
    }

    const returned = useMemo(() => {
        cleanupRef.current && cleanupRef.current();
        cleanupRef.current = null;

        const [returned, cleanup] = callback();
        cleanupRef.current = typeof cleanup === "function" ? cleanup : null;

        return returned;
    }, deps);

    return returned;
}

Notice: The first version of a component's cleanup method may be called after the initiation method of the second version of it. In React StrictMode, this occurs on mounting a component since, for testing, it runs twice. e.g.,

memo for :r0:
memo for :r1:
memo cleanup for :r0:

(Note that, due to GC behavior, it may even happen much later, and in no guaranteed order)

But again, if your setup and cleanup logic is pure, this shouldn't be problematic.

Sorilda answered 10/11, 2023 at 3:48 Comment(3)
perfect. React has so many gotchas around useEffect and useMemo that they never actually behave as a developer expects.Plunder
This would cleanup at some later date depending on the gc cycle, right? If I have some very large data, is there a different approach that would ensure only one copy of the data per component sticks around? Like maybe combine this with a prevRef, and the first thing the factory function does is destroy any existing data?Obliterate
@MHebes, yes, sadly there's no guarantee when it cleans up. But it does tend to happen sooner if your app is memory intensive. If you only need one instance, it's probably best to declare it outside of your components in general. But if you do need it per-component, you may be better off using useEffect which guarantees immediate cleanup on unmount / rerender. It also guarantees that it will only run if the component is actually visible (which might help if initialization hits performance). Only disadvantage is you can't immediately use the state in the rendered JSX.Sorilda
O
2

clean up effects for hooks you execute when a component will be unmounted. It's performed by a returned function at your useEffect hook:

useEffect(() => {
  // return a function to be executed at component unmount
  return () => object.destroy()
}, [object])

Note: as @Guillaume Racicot pointed out, in some cases it's possible the object not being created yet by the time unmount is executed, hence you would face an error. In this case remember to conditionally executing destroy, you could use optional chaining object?.destroy() for that.

Overvalue answered 2/3, 2021 at 20:40 Comment(10)
That's not an answer to the question. The question was: how to clean up something while using memoizationSeiber
@Seiber your comment doesn't make much sense. to execute a clean up effect on dismount (dead component like OP asked) you should return a function with the proper expected action (cat.destroy() in this case). the fact that the object is memoized or not is irrelevantOvervalue
This won't work in react 18Vaish
@GuillaumeRacicot why is that? useEffect signature didn't change in React 18.Overvalue
@Overvalue effects are run twice on mount in react 18, so this destroy will be called on mount before the second effect run.Vaish
@GuillaumeRacicot effects don't run twice in react 18, the React.StrictMode functionality forces that. React.StrictMode works only in development environment and not in production. And its usage is also optional. I suggest you to read more about in the React docsOvervalue
@Overvalue Strict mode does that only because it would cause bugs when an effect is misbehaving. So the strict mode actually flag this effect as being broken by calling object.destroy() on the first render.Vaish
that sounds reasonable, I just edited it.Overvalue
useEffect has quite a different lifecycle to useMemo or useRef. Let's quote the React documentation: "Your cleanup logic should be “symmetrical” to the setup logic ... If you have cleanup code without corresponding setup code, it’s usually a code smell". This answer falls right into the trap.Barrie
I think what @Barrie is getting at above is that useMemo is intended for caching purposes only. It's mentioned in the caveats section of the docs. React could, e.g., throw away the cached value for various reasons in the future. This could mean you could get two new Cat() calls with only one cleanup call from a useEffect cleanup function. Guaranteeing equal cleanups to memo calls would require more complexity.Silma
E
0

After days of researching, I found this question is wrong, as React team expects useMemo hook to be used without side effects.

After Cat is created, if it needs to be destroyed, it means it has side effects, so it should not be created by useMemo.

FinalizationRegistry as @Codesmith suggests won't work, because when cat has side effects, it is very likely to be used somewhere else, so that you need to explicitly release it. In such scenario, it won't be finalized at all, FinalizationRegistry will not be triggered, it would cause memory leaks (although it may not be so harmful).

What React team want's you to do is wrapping all the side effects in useEffect hook. Therefore, the correct usage should be:

const [cat, setCat] = useState();

useEffect(() => {
  const c = new Cat();
  setCat(c);
  return () => c.destroy();
}, [])

if (cat === undefined) {
  return undefined;
}

// render the cat

The "weird" behavior of useEffect in strict mode just shows we are using it wrong and it violates the React rule. Instead we should avoid them.

Extrajudicial answered 7/8 at 14:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.