Lance Pollard
Lance Pollard

Reputation: 79278

How to implement your own useMemo from Scratch, for React

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.

Upvotes: 6

Views: 5300

Answers (2)

NiRUS
NiRUS

Reputation: 4259

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!

Upvotes: 5

Whatabrain
Whatabrain

Reputation: 260

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.

Upvotes: 6

Related Questions