5

Basically this:

function MyComponent() {
  let [count, setCount] = useState(1)
  let memo = useMyMemo(() => new MyClass)

  return <div onClick={update}>{count}</div>

  function update() {
    setCount(count + 1)
  }
}

function useMyMemo(fn) {
  // what to do here?
}

class MyClass {

}

I would like for useMyMemo to only return 1 instance of the class per component instance. How do I set this up to implement it without resorting to using any of the existing React hooks? If it's not possible without the React hooks, they why not? If it is possible only through accessing internal APIs, how would you do that?

As a bonus it would be helpful to know how it could be passed property dependencies, and how it would use that to figure out if the memo should be invalidated.

Lance
  • 75,200
  • 93
  • 289
  • 503
  • 1
    Not an answer, but you'll need to dig into React internals to do this. Hooks work because React knows what component function it's currently calling. `useState` and such use that internal information to do their work (see the `resolveDispatcher` calls throughout the [`ReactHooks.js` file](https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js)), they're basically wrappers for calls on a "dispatcher" object. One such dispatcher is implemented [here](https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.js). – T.J. Crowder Aug 10 '19 at 10:55

2 Answers2

5

I think you're describing how useMemo already works, assuming you pass [] as the dependencies parameter. It should already create one instance of MyClass per instance of MyComponent. However, the documentation says that future versions of useMemo will not guarantee that the value is only called once, so you might want to try useRef.

const memo = useRef(null);
if (!memo.current) {
  memo.current = new MyClass()
}

If you want it to create a new instance of MyClass when dependencies change, you'll have to either use useMemo (and accept the chance that the value might be invalidated occasionally), or do your own shallow comparison against the previous value. Something like this:

const useMyMemo = (create, dependencies) => {
  const val = React.useRef(create());
  const prevDependencies = React.useRef([]);
  if (!shallowEquals(dependencies, prevDependencies.current)) {
    val.current = create();
    prevDependencies.current = [...dependencies];
  }
  return val;
};

I'll leave the implementation of shallowEquals to you, but I believe lodash has one.

Now you really asked for a function that doesn't use any React hooks, and mine uses useRef. If you don't want useRef, you can create your own simple memoization function that always returns the same pointer regardless of changes to .current.

Whatabrain
  • 238
  • 4
  • 7
  • you should not write to ref in render: https://beta.reactjs.org/learn/referencing-values-with-refs#best-practices-for-refs – Giorgi Moniava Jul 20 '22 at 10:04
  • @GiorgiMoniava True. In this case, I'm just working around the fact that useRef has no initializer function. I could have had "useRef(new MyClass())," but that would construct the class every time. In other words, this is not a normal case of writing to refs in a render, but rather just an optimization of the normal way to initialize refs. useMyMemo writes to refs in the renderer too. It's hacky, but it answers the question of how to memoize complex data without using useMemo. – Whatabrain Jul 21 '22 at 15:26
  • actually I was referring to usage inside `useMyMemo `, the first usage (with `MyClass`) is actually AFAIK permitted - check the link. But maybe it is safer in older react versions, can't say, the link also doesn't explain well why it is a bad idea. – Giorgi Moniava Jul 21 '22 at 16:15
  • @GiorgiMoniava It's a bad idea because changing the value of a ref doesn't trigger a rerender. If the component happens to rerender for some other reason, the value will be used, but otherwise, it has no effect. This can lead to inconsistent and unexpected results. `useMyMemo` has that same problem. However, it does create a consistent, memoized pointer that's only updated when dependencies change, which is what the poster seemed to want. I never said it was a good idea; just that it answered the poster's question. :) – Whatabrain Jul 24 '22 at 01:44
  • it can be a problem in concurrent mode AFAIK: https://stackoverflow.com/questions/68025789/is-it-safe-to-change-a-refs-value-during-render-instead-of-in-useeffect/68025947#68025947. Anyway, I don't know more about this myself, so we can leave it in comments, if someone is interested they can find out. – Giorgi Moniava Jul 24 '22 at 15:59
2

Simple mock implementation of useMemo using useRef.

Note: You can shallowCompare or deepCompare to compute isChanged based on your need. Example if dependencies are objects.

/* File: useMemo.js */

import { useRef } from "react";

export function useMemo(callback, deps) {
  const mPointer = useRef([deps]); // Memory pointer (Make sure to read useRef docs)
  const isChanged = mPointer.current[0].some(
    // If dependencies changed
    (item, index) => item !== deps[index]
  );

  if (mPointer.current.length === 1 || isChanged) {
    // If first time or changed, compute and store it
    mPointer.current = [deps, callback()];
  }

  return mPointer.current[1];
}

See CodeSandbox demo.

Read: useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.


Pure javascript mock using closure - [ For educational purpose ONLY! ]

/* File: useMemo.js */

function memo() {
  const stack = {};
  return (callback, deps) => {
    /* Creating hashkey with dependencies + function source without whitespaces */
    const hash = deps.join("/") + "/" + callback.toString().replace(/\s+/g, "");

    if (!stack[hash]) {
      stack[hash] = callback();
    }

    return stack[hash];
  };
};

export const useMemo = memo();

See CodeSandbox demo.

Idea explaination : With above code trying to create a unique hash by combining dependencies value & function source using toString, removing spaces in string to make hash as unique as possible. Using this as a key in the hashmap to store the data and retrieve when called. The above code is buggy, for example callback result that are cached are not removed when component unmount's. Adding useEffect and return clean-up function should fix this during unmount. Then this solution becomes tightly coupled with react framework

Hope this helps. Feedback is welcomed to improve the answer!

NiRUS
  • 3,901
  • 2
  • 24
  • 50