0.0.1 • Published 3 years ago

use-dispose-uncommitted v0.0.1

Weekly downloads
2
License
MIT
Repository
github
Last release
3 years ago

Warning ⚠️☣️🐉 you probably don't need it

For the vast majority of cases, use useEffect, and the cleanup function for side effects and cleanups, and KEEP YOUR RENDER FUNCTION PURE

Ah, still here? useDisposeUncommitted

codecov
useDisposeUncommitted is a tiny React hook to help us clean side effects of uncommitted components.
Based on a similar implementation of MobX's internal hook.
Relevant discussion in the React repo on this topic: #15317 [Concurrent] Safely disposing uncommitted objects.

When do we ever need that? - Explainer

It is known that side effects in React should only be inside useEffect and that React will run them only when the component instance is actually being committed/mounted.

However, React may decide to throw away a component instance after rendering (for various reasons like suspense, strict mode, aborted renders), but before running useEffect, without letting us know by any means.

As long as we keep side-effects in useEffect, it's not a problem. But consider MobX as an example: in order to track access to observables, MobX must create the reaction during the render phase. In this case, React may throw away the component instance, and we won't have a chance to dispose of the MobX reaction, resulting in memory leaks and possible bugs.

Simple usage example

Install the package: yarn add use-dispose-uncommitted.

// default import also works
import { useDisposeUncommitted } from "use-dispose-uncommitted";

function MyComponent() {
  useDisposeUncommitted(function disposer() {
    console.log('Component disposed by React');
  }, function reviver(revivedFromRenderBeforeCommit) {
    if (revivedFromRenderBeforeCommit) {
      console.log('Component speculatively disposed by us, but React suddenly re-rendered it');
    } else {
      console.log('Component speculatively disposed by us, but React suddenly mounted it');
    }
  });
}

Naive Mobx's useObserver implementation:

import { Reaction } from "mobx";
function useObserver(jsxFactoryFunction) {
  const [__, forceUpdateCounter] = useState(0);

  function forceUpdate() {
    forceUpdateCounter(c => c + 1);
  }

  const reactionRef = useRef<Reaction>(null);
  const mountingRef = useRef<{ changedBefore: boolean, isMounted: boolean }>({
    changedBefore: false,
    isMounted: false
  });

  function createReaction() {
    reactionRef.current = new Reaction(() => {
      if (mountingRef.isMounted) {
        forceUpdate();
      } else {
        mountingRef.changedBefore = true;
      }
    });
  }

  useDisposeUncommitted(() => {
    reactionRef.current.dispose();
    reactionRef.current = null;
  }, (revivedFromRenderBeforeCommit) => {
    createReaction();
  });

  useLayoutEffect(() => {
    if (mountingRef.changedBefore) {
      forceUpdate();
    }
    mountingRef.isMounted = true;

    return function unMountCleanup() {
      reactionRef.current.dispose();
      reactionRef.current = null;
    }
  }, []);

  if (reactionRef.current === null) {
    createReaction();
  }
}

Implementation details

Using FinalizationRegistry

In JS engines which support FinalizationRegistry (chromium 84+, firefox 79+, node 14), we allocate a React state without creating any external references, and we register it for cleanup using a FinalizationRegistry.
As only React has a reference to that state object, we can tell that React has disposed of the component when the cleanup callback is called for that state object.

Using timer based GC

For platforms without FinalizationRegistry, we take a speculative approach: if during a specific period of time since the render the component wasn't committed, we assume it was disposed by read. That period of time is a hard-coded 10 seconds, non configurable, matching the MobX implementation.

Component revival

This speculation can result in a situation, where we've ran our disposer, but React suddenly commits or re-renders the component. For this case, we have the reviver function, which runs to signal that.

Not sure if it's a good idea to use FinalizationRegistry for that? Read this comment

Testing, Running, Building.

This project is using yarn 2 + pnp + zero install. You need to have yarn installed (version doesn't matter), clone and run yarn test or yarn build.
Node 14+ is required to run the tests suite.

Tests coverage

Some code paths are not covered by tests (revive before mount). We need to investigate how to trigger that code path.

Code Of Conduct (As MobX's)

Code Of Conduct