Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React useMemo memory clean

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

const object = useMemo(() => {
  return new Cat()
}, [])
like image 815
Иван Вольнов Avatar asked Apr 26 '26 04:04

Иван Вольнов


1 Answers

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;
}

GC Disclaimer:

Due to Garbage Collection behavior, the first version of a component's cleanup method may be called after the initiation method of the second version of it - and, in reality, it's oftentimes much later. There is also no guaranteed order that the cleanup methods will be called in.

In React StrictMode, this occurs on mounting a component since, for testing, it runs twice. e.g.,

memo for :r0:
memo for :r1:
// some time later
memo cleanup for :r0:

If your setup and cleanup logic is pure, this probably shouldn't be problematic. But it may be important to keep in mind. The implementation does, however, clean up values immediately when the same component re-renders but the dependencies changed.

like image 82
Codesmith Avatar answered Apr 27 '26 19:04

Codesmith



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!