React useMemo memory clean

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

const object = useMemo(() => {
  return new Cat()
}, [])

Upvotes: 9

Views: 11639

Answers (3)

D-Y
D-Y

Reputation: 145

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.

Upvotes: 1

Codesmith
Codesmith

Reputation: 6752

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.

Upvotes: 7

buzatto
buzatto

Reputation: 10382

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.

Upvotes: 2

Related Questions