A common scenario in the React project is:

const [watchValue, setWatchValue] = useState(' ');
const [otherValue1, setOtherValue1] = useState(' ');
const [otherValue2, setOtherValue2] = useState(' ');

useEffect(() = > {
    doSomething(otherValue1, otherValue2);
}, [watchValue, otherValue1, otherValue2]);
Copy the code

We want to doSomething when watchValue changes, which will reference other values otherValue1, otherValue2.

Then there was the vexing question:

  • If you don’totherValue1, otherValue2If I add a dependency array,doSomethingIt’s probably going to be accessedotherValue1, otherValue2Old variable references that can cause unexpected errors (warnings if hooks related esLint is installed).
  • On the other hand, if you takeotherValue1, otherValue2When you add a dependency array, these two values changedoSomethingAlso executes, which is not what we want (we just want to reference their values, but don’t want them to fire)doSomething).

To solve this problem, change otherValue1, otherValue2 to ref:

const [watchValue, setWatchValue] = useState(' ');
const other1 = useRef(' ');
const other2 = useRef(' ');

// ref can be left out of the dependent array because the reference is unchanged
useEffect(() = > {
    doSomething(other1.current, other2.current);
}, [watchValue]);
Copy the code

Other1, other2, other2, other1, other2, other1, other2, other1, other2, other2, other1, other2, other2, other2, other1, other2 UseRef does not trigger component rendering when useRef current changes, so the interface does not update when useRef current changes.


This is a problem with hooks, the useState variable triggers rerendering, keeps the interface up to date, but as a useEffect dependency it always triggers the execution of unwanted functions. The useRef variable can be relied upon as a useEffect without triggering component rendering or interface updates.

How to solve it?

You can combine the features of useRef and useState to construct a new hooks function: useStateRef.

import { useState, useRef } from "react";

// Use useRef's reference properties while keeping useState responsive
type StateRefObj<T> = {
  _state: T;
  value: T;
};
export default function useStateRef<T> (
  initialState: T | (() => T)
) :StateRefObj<T> {
  // Initialize the value
  const [init] = useState(() = > {
    if (typeof initialState === "function") {
      return (initialState as () => T)();
    }
    return initialState;
  });
  // Set a state that triggers component rendering
  const [, setState] = useState(init);
  
  // When reading value, the latest value is fetched
  // When value is set, setState is triggered to render the component
  const [ref] = useState<StateRefObj<T>>(() = > {
    return {
      _state: init,
      set value(v: T) {
        this._state = v;
        setState(v);
      },
      get value() {
        return this._state; }}; });// Returns a reference variable that does not change throughout the lifecycle of the component
  return ref;
}

Copy the code

In this way, we can use:

const watch = useStateRef(' ');
const other1 = useStateRef(' ');
const other2 = useStateRef(' ');

// Change the value: watch.value = "new";

useEffect(() = > {
    doSomething(other1.value, other2.value);
   // These three values are all reference variables, which are invariant throughout the life of the component
   // The React hooks esLint plugin only recognizes useRef as a reference, does not warn, adds it for safe references
}, [watch.value, other1, other2]);
Copy the code

In this way, watch, other1, and other2 have useRef references and do not trigger unnecessary doSomething execution. With useState’s responsiveness, changes to.value trigger component rendering and interface updates.

We want to add watch.value to the dependency array when a variable change triggers doSomething. When we only want to reference the value without it triggering doSomething, we add the variable itself to the array.

Note: the use of. Value instead of. Current is intended to keep it distinct from useRef.