简体   繁体   中英

Why react `useRef` hook stores the objects in `current` property? Why cant it store directly in the ref object?

Why does the object returned by the useRef hook stores whatever value it's supposed to hold in current property? Why can't we assign something directly into the ref object as shown below:

const sampleRef = useRef([]);

/** why can't we do this... */
sampleRef.push('1');

/** ...instead of this? Why an extra `current` object? */
sampleRef.current.pus('1');

What is the purpose of useRef returning the argument wrapped inside another object with current property?

The answer to this question is not specific to React and its hooks system. Mutating an object is just a solution to how you share values between different closures / scopes.

When you call useRef() for your component a "ref object" is created in React internals, tied to that particular instance of your component. Each time useRef() is called across multiple renders, the same "ref object" is returned. Setting current on it is how your store a value to re-access it on next render.

By doing something like

let value = useRef();
value = 1234;

you're throwing away the ref object, replacing it with a new value in your local scope. There's no way React can track that action and update the ref object that is stored in its internals. (In fact React does not track any action to "ref objects" anyway, it just gives them to you. You mutate them, you access them).

But with the current API, you do

const ref = useRef(); // ref is an object that is stored somewhere in React internals
ref.current = 1234; // you update the property of that object

The next time your component renders, React gives you the same ref object, so that you can use the value that you set previously.

As I understood, they made it because they needed to create an object in order to seal the DOM element object (in development mode) and memoize it. As you know if we are going to memoize something, we need to convert it to an object or array .

Reference:


function mountRef<T>(initialValue: T): {current: T} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  if (__DEV__) {
    Object.seal(ref);
  }
  hook.memoizedState = ref;
  return ref;
}

https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.js#L916

The react hooks system works with immutable values. Whenever the component is rendered, the hooks are called (for example the useState hook), and they produce a value or two (state and setter function). If this values are changed from the previous values, other hooks might be called ( useEffect when the setter function is initialised).

However, sometimes we don't want to react to this changes. We don't really care what is the value as long as it's there, and we don't care if something changes it. For this cases we've got the ref :

The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class.

Whenever you need to store a value, that will be used, but won't cause a re-render, nor cause useMemo , useCallback , useEffect , etc... to recompute, you can set that value via a ref. Since the ref itself will be used as part of hooks dependencies ( useMemo(() => {}, [ref] ), you can't update it. To enable immutability, a property inside that ref object, can be changed ref.current , without causing the dependant to recompute, since it's the same ref.

I'm trying to answer the root cause of your question, which I interpret to be something like: "It seems that we should not need an intermediate object with a .current property." You're correct. We don't. Whatever the actual reason regarding useRef , I have noticed it is possible to instead do something like the following, which has the kind of syntax you were asking for in your question (the elimination of .current ):

//note foo is in array brackets by itself
const [foo /*no setFoo here*/]= useState({bar:"baz"});
...
foo.bar="hello"

or

//note foo in brackets by itself
const [foo /*no setFoo here*/]= useState([]);
...
foo.push(1);

This lets us directly mutate foo 's properties without using .current . As long as we never call setFoo , mutating foo 's properties will not cause a rerender on its own. The value of foo itself is never changed, since it always points to the same object or array.

However, it is possible, just as with any other variables including useRef ones, to cause a useEffect hook to rerun after a rerender if a changed property like foo.bar appears in the 2nd argument array of the useEffect .

I haven't tried let [foo]= useState("whatever") yet. In that case, we'd be altering the actual value of foo , and depending on React to give that altered value back to us on subsequent rerenders, even though we never notified React of the change. Seems sketchy.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM