Intro

This article will try to understand the internal operation logic of React Hook by starting with a common stale state problem.

Without further ado, let’s go straight to the example Sandbox.

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() = > {
    let id = setInterval(() = > {
      setCount(count + 1);
    }, 1000);
    return () = > clearInterval(id); } []);return <h1>{count}</h1>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);
Copy the code

Can you see any problems in the sample code? (If it does, it probably won’t pay to continue reading.) The actual effect of this code is that the page changes from 0 to 1, and then displays 1 all the time. It’s not as intuitive as updating every second.

SetCount can also accept a Function

Use setState(prevState => newState) to get the latest setState.

Indeed, this is also supported by the Hooks Dispatch method. In the example above, just setCount(count + 1); Rewrite to setCount(val => val + 1) to work as expected.

Why do the two counts differ?

What is the reason for the divergence of understanding? In the same JS method, read the same variable in different locations, get inconsistent results.

Add print count in the next example, and you’ll be surprised to see that each time you rerender, the count read is the latest, while setInterval is still 0 each time. And they all appear in the same scope, and there’s only one count in scope, so it makes sense to read the latest count every time, right?

const [count, setCount] = useState(0);
console.log('render val:', count)

useEffect(() = > {
    let id = setInterval(() = > {
      console.log('interval val:', count)
      setCount(val= > val + 1);
    }, 1000);
    return () = > clearInterval(id); } []);Copy the code

To clarify this problem, we have to put the “What is a closure?” Pull out.

The function is bundled together with references to its surrounding state (lexical environment) to form a closure. That is, closures allow you to access outer function scopes from inner functions. In JavaScript, whenever a function is created, a closure is generated when the function is generated.

In this simple example, where does the closure occur? UseEffect first parameter, setInterval first parameter. In both cases, the program creates a new Function and passes it as a Function argument. Because of the closure, the external variable (count) can only be retrieved when the Function is actually run.

Since we passed useEffect a second parameter [], this effect has no external dependencies and only needs to be run on the first render, setInterval will only be registered once, no matter how many times the component is rerendered.

And the Function Component reexecutes the Function each time it render. The closure created first has nothing to do with the closure created second time.

So, when the program is running, the closure in setInterval always references the original count, while useState gets the latest count. This is the root cause of the inconsistency between the printed results of the two codes.

UseEffect is used only once. UseEffect [] is used only once.

const [count, setCount] = useState(0);
console.log('render val:', count)

useEffect(() = > {
    let id = setInterval(() = > {
      console.log('interval val:', count)
      setCount(val= > val + 1);
    }, 1000);
    return () = > clearInterval(id);
});
Copy the code

Interval can read the latest count by running the code above.

The principle is that this effect is now executed again each time the render is rerendered, producing a new closure that references the latest count. But this works because the only action that triggers rerender happens to be setCount, and when there are multiple actions that trigger render, more results are produced.

Why can useState read the latest value when it is re-rendered every time

UseState gets the latest value since render is new every time.

Tracing back to the React renderWithHooks source code, you can see that the Function Component is actually called as a normal method when rendered.

export function renderWithHooks<Props.SecondArg> (
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderExpirationTime: ExpirationTime,
) :any {
  // ...
  let children = Component(props, secondArg);
  / /...
  return children;
}
Copy the code

When the component is invoked, the useState method is executed. Look from the react source, the react internal maintains a hook chain table, head exist currentlyRenderingFiber chain table. MemoizedState, node through the next link.

Two pieces of code related to the useState hook:

// First render useState hook
function mountState<S> (
  initialState: (() => S) | S,
) :S.Dispatch<BasicStateAction<S> >]{
  const hook = mountWorkInProgressHook(); // Create a new hook to mount to the end of the list
  hook.memoizedState = hook.baseState = initialState;
  // ...
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  // Return the cached memoizedState (here initialState)
  return [hook.memoizedState, dispatch];
}

// Execute when updating render useState hook
function updateState<S> (
  initialState: (() => S) | S,
) :S.Dispatch<BasicStateAction<S> >]{
  // useState is also implemented based on reducer
  return updateReducer(basicStateReducer, (initialState: any));
}

function updateReducer<S.I.A> (reducer: (S, A) => S, initialArg: I, init? : I => S,) :S.Dispatch<A>] {
  const hook = updateWorkInProgressHook(); // Get the cached hook
  // ...
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  // Return the cached memoizedState
  return [hook.memoizedState, dispatch];
}
Copy the code

The React source code handles first execution and update of hooks separately, but the logic is the same, acquiring or creating a Hook, exposing external memoizedState and a dispatch method. MemoizedState is changed when The Dispatch call is made. This is why you can read the latest Value every time you render useState.

useRef

So back to the original question, what do I do if I want to read the correct count in setInterval?

The other hook, useRef code, is here.

const [count, setCount] = useState(0);
const countRef = useRef(count);

useEffect(() = > {
  // Update count in time
  countRef.current = count;  
});

console.log('render val:', count)

useEffect(() = > {
    let id = setInterval(() = > {
      // Instead of reading count directly, read countref.current
      console.log('interval val:', countRef.current)
      setCount(val= > val + 1);
    }, 1000);
    return () = > clearInterval(id); } []);Copy the code

With useRef, give countref. current = count the latest value each time; Change the location inside the closure where count was retrieved to countref.current. The closure refers to an Object, and when it is actually run it is through a reference to the Object rather than the value of an underlying data type.

Print result:

How is useRef implemented internally? UseRef caches an Object directly on the hook, and rerenders the same Object each time.

function mountRef<T> (initialValue: T) :{|current: T|} {
  const hook = mountWorkInProgressHook();
  const ref = { current: initialValue };

  hook.memoizedState = ref;
  return ref;
}

function updateRef<T> (initialValue: T) :{|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}
Copy the code

Another common scenario for this problem is the callback event. For example, in ReactNative, the component that encapsulated a gesture processing based on PanResponder triggers the onChange callback event when the condition is met. If you are not careful, the onChange is not the latest problem.

function SwipeToConfirm ({ onChange }) {
    const onChangeRef = useRef(onChange)
    useEffect(() = > {
        onChangeRef.current = onChange
    })
    const panResponder = useRef(PanResponder.create({
      / /...
      onPanResponderRelease: (evt, gestureState) = > {
          // Execute onChange if conditions are met
          onChangeRef.current()
      }
    })).current;

    return (
      <Animated.View
        {. panResponder.panHandlers}
      >
      </Animated.View>)}Copy the code

πŸ™†β™‚οΈ, see here I believe you have a deeper understanding of Hook.

Recommended reading

  • Overreacted. IO/useful – Hans/mak…
  • medium.com/@ryardley/r…