The original link

preface

This article analyzes the advantages and disadvantages of React Hook apis from the perspective of Mini React — Preact source code. To understand why hooks are used and how best to use them.

Article 2 the rules

Why is that?

  1. ✅ use hooks only at the top level and do not call hooks in loops, conditions, or nested functions;
  2. ✅ only call hooks in React functions, not in normal JavaScript functions.

Source code analysis

let currentIndex; // Global index
let currentComponent; // The component where the current hook is located

function getHookState(index) {
  const hooks =
    currentComponent.__hooks ||
    (currentComponent.__hooks = {_list: []._pendingEffects: []});

  if (index >= hooks._list.length) {
    hooks._list.push({});
  }
  return hooks._list[index];
}
Copy the code
// Get the registered hook
const hookState = getHookState(currentIndex++);
Copy the code
  • Hook states are maintained in an array structure, executedhook apiWhen the indexcurrentIndex + 1Put them in arrays one by one. When the componentrenderBefore, it callshook render, reset the index and set the current component, hook injection inoptionsInside.
options._render = vnode= > {
  currentComponent = vnode._component;
  currentIndex = 0;
  // ...
};
Copy the code
  • The first thing to remember is that a function component reexecutes the entire function every time it diff, whereas a class component only executes this.render. Therefore, hooks suffer a performance loss. Provides useMemo and useCallback optimizations.

  • Hook in each render, the last hook state, if executed in the loop, conditional or nested function of the uncertain branch, it is possible to fetch wrong data, resulting in chaos.

function Todo(props) {
  const [a] = useState(1);
  if(props.flag) {
    const [b] = useState(2);
  }
  const [c] = useState(3);
  // ...
}
Copy the code
<Todo flag={true} / >Copy the code
  • At this timea = 1, b = 2, c = 3;
<Todo flag={false} / >Copy the code
  • When the conditions are changed,a = 1, c = 2cWrong state!

The react component and its lifecycle are parasitic on hooks.

  • Preact hookoptionsObject_render -> diffed -> _commit -> unmountFour hooks, each executed before the life cycle of the object component, are less intrusive.

useState

use

/ / declare the hooks
const [state, setState] = useState(initialState);
/ / update the state
setState(newState);

// Functional updates are also available
setState(prevState= > { // Get the last state value
  // You can also use object.assign
  return{... prevState, ... updatedValues}; });Copy the code
  • Lazy initial state. If you initializestateValues are expensive, functions can be passed in, and initialization is performed only once.
const [state, setState] = useState((a)= > {
  const initialState = someExpensiveComputation(props);
  return initialState;
});
Copy the code
  • Skip state updates. Set the same value (Object.isDetermine), does not trigger component updates.
const [state, setState] = useState(0);
// ...
// Updating state does not trigger component re-rendering
setState(0);
setState(0);
Copy the code

Why is that?

  • Pit: rely onprops.state === 1Initialize thehookWhy,props.state === 2When,hook stateNo change?
function Component(props) {
  const [state, setState] = useState(props.state);
  // ...
}
Copy the code
  • What is the principle of lazy initialization?
  • hook stateHow do changes drive component rendering, and why are they acceptableclass stateUse?

Source code analysis

  • PreactuseStateIs the use ofuseReducerImplementation, easy to write, the code will be slightly modified.
function useState(initialState) {
  const hookState = getHookState(currentIndex++);
  if(! hookState._component) { hookState._component = currentComponent; hookState._value = [ invokeOrReturn(undefined, initialState),

      action => {
        const nextValue = invokeOrReturn(hookState._value[0], action);
        if (hookState._value[0] !== nextValue) {
          hookState._value[0] = nextValue; hookState._component.setState({}); }}]; }return hookState._value;
}
Copy the code
// Utility functions to support functional initialization and updating
function invokeOrReturn(arg, f) {
  return typeof f === 'function' ? f(arg) : f;
}
Copy the code
  • It can be seen thatuseStateOnly the first time in the componentrenderIs initialized once, and the status is later updated by the returned function.
  1. Pit: Initializations (including passed functions) are performed only once and should not be relied uponpropsTo initializeuseState;
  2. Optimization: You can use passed in functions to optimize performance for expensive initialization operations.
  • hookState._value[0] ! == nextValueCompare old and new values to avoid unnecessary rendering.
  • As you can see, the update operation takes advantage of the component instance’sthis.setStateFunction. That’s whyhookCan replaceclassthis.stateUse.

useEffect

use

  • For example, the common basisqueryIf the component is loaded for the first time, only one request is sent.
function Component(props) {
  const [state, setState] = useState({});
  
  useEffect((a)= > {
    ajax.then(data= >setState(data)); } []);/ / dependencies
  // ...
}
Copy the code
  • useStateHave said,propsThe initialstateThere are pits. You can use themuseEffectThe implementation.
function Component(props) {
  const [state, setState] = useState(props.state);
  
  useEffect((a)= > {
    setState(props.state);
  }, [props.state]); // props. State changes the value to state
  // ...
}
Copy the code
  • Clear side effects, such as listening to resize the browser window, and then clear side effects
function WindowWidth(props) {
  const [width, setWidth] = useState(0);

  function onResize() {
    setWidth(window.innerWidth);
  }
  // Perform side effects only once and the component will be cleared when unmounted
  useEffect((a)= > {
    window.addEventListener('resize', onResize);
    return (a)= > window.removeEventListener('resize', onResize); } []);return <div>Window width: {width}</div>;
}
Copy the code
  • Note: inuseEffectIn the use ofstateIt is best to rely on it, otherwise it is easy to producebug
function Component() {
  const [a, setA] = useState(0);
  useEffect((a)= > {
    const timer = setInterval((a)= > console.log(a), 100);
    return (a)= > clearInterval(timer)
  }, []);
  return <button onClick={()= > setA(a+1)}>{a}</button>
}
Copy the code

When you click the button A +=1, console.log still prints 0. This is because the useEffect side effect will only be the _pendingEffects array when the component is first loaded, forming a closure.

Modified as follows:

function Component() {
  const [a, setA] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => console.log(a), 100);
    return () => clearInterval(timer)
-} []);
+ }, [a]);
  return <button onClick={() => setA(a+1)}>{a}</button>
}
Copy the code

This code runs in React, and the output changes when a button is clicked. In Preact, the timer is not cleared, indicating a bug. -_ – | |

Why is that?

  • useEffectWhat problem was solved

Generally send data requests to componentDidMount, after which componentWillUnmount is cleaned up in relation. This results in unrelated logic being intermingled with componentDidMount, while the corresponding cleanup work is assigned to componentWillUnmount.

With useEffect, you can write independent logic in different Useeffects without having to worry about cleaning up other blocks of code while worrying about maintenance.

  • Is performing side effects (changing the DOM, adding subscriptions, setting timers, logging, etc.) inside a component function not allowed?

Every time the diff function component is used like the this.render function of the class component, the whole is executed, and the side effects of operating in the body are fatal.

  • useEffectThe mechanism?

Source code analysis

function useEffect(callback, args) {
  const state = getHookState(currentIndex++);
  if(argsChanged(state._args, args)) { state._value = callback; state._args = args; currentComponent.__hooks._pendingEffects.push(state); }}Copy the code
  • Utility functions with a dependency ofundefinedOr a value in the dependency array changes, thentrue
function argsChanged(oldArgs, newArgs) {
  return! oldArgs || newArgs.some((arg, index) = >arg ! == oldArgs[index]); }Copy the code
  • The callback function that shows the side effects will be in_pendingEffectsArray maintenance, code is executed in two places
options._render = vnode= > {
  currentComponent = vnode._component;
  currentIndex = 0;

  if (currentComponent.__hooks) { // Why do you need to clean it up?!currentComponent.__hooks._pendingEffects.forEach(invokeCleanup); currentComponent.__hooks._pendingEffects.forEach(invokeEffect); currentComponent.__hooks._pendingEffects = []; }};Copy the code
function invokeCleanup(hook) {
  if (hook._cleanup) hook._cleanup();
}

function invokeEffect(hook) {
  const result = hook._value(); // If a side effect function returns a function, it is saved as a cleanup function.
  if (typeof result === 'function') hook._cleanup = result;
}
Copy the code
options.diffed = vnode= > {
  const c = vnode._component;
  if(! c)return;

  const hooks = c.__hooks;
  if (hooks) {
    if(hooks._pendingEffects.length) { afterPaint(afterPaintEffects.push(c)); }}};Copy the code
function afterPaint(newQueueLength) {
  if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) {
    prevRaf = options.requestAnimationFrame;
    (prevRaf || afterNextFrame)(flushAfterPaintEffects);
  }
}
Copy the code
function flushAfterPaintEffects() {
  afterPaintEffects.some(component= > {
    if (component._parentDom) {
      try {
        component.__hooks._pendingEffects.forEach(invokeCleanup);
        component.__hooks._pendingEffects.forEach(invokeEffect);
        component.__hooks._pendingEffects = [];
      } catch (e) {
        options._catchError(e, component._vnode);
        return true; }}}); afterPaintEffects = []; }Copy the code
  • I suspect that options._render code is copied from flushafterpaint ects without thinking about it. Resulting in a bug mentioned above.

  • AfterPaint uses requestAnimationFrame or setTimeout for the following purposes

Unlike componentDidMount and componentDidUpdate, the function passed to useEffect is delayed after the browser has laid out and drawn, and does not block the browser update screen. (erratum: useEffect in React does this, Preact does not)

useMemo

use

function Counter () {
  const [count, setCount] = useState(0);
  const [val, setValue] = useState(' ');
  const expensive = useMemo((a)= > {
    let sum = 0;
    for (let i = 0; i < count * 100; i++) {
      sum += i;
    }
    return sum
  }, [ count ]); // ✅ The callback is executed only if the count changes

  return (
    <>
      <span>You Clicked {expensive} times</span>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <input value={val} onChange={event => setValue(event.target.value)} />
    </>
  )
}
Copy the code

Why is that?

  • useMemoWhat problem was solved

As stated above, function components should be executed repeatedly, which can cost performance if done at a high cost. React provides useMemo to cache the results of function execution and useCallback to cache functions.

Source code analysis

function useMemo(factory, args) {
  const state = getHookState(currentIndex++);
  if (argsChanged(state._args, args)) {
    state._args = args;
    state._factory = factory;
    return (state._value = factory());
  }

  return state._value;
}
Copy the code
  • As you can see, the function passed in is simply executed according to the dependency and the result is stored in the internal hook state.

  • Remember that all hook apis are the same, do not use state in side leases without passing it in as a dependency.

useCallback

use

const onClick = useCallback(
  (a)= > console.log(a, b),
  [a, b]
);
Copy the code

Why is that?

  • useCallbackWhat problem was solved

As mentioned above, it’s used to cache functions

  • For example, the example above optimizes the listening window.
function WindowWidth(props) {
  const [width, setWidth] = useState(0);

- function onResize() {
- setWidth(window.innerWidth);
-}
  
+ const onResize = useCallback(() => {
+ setWidth(window.innerWidth);
+} []);useEffect(() => { window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); } []); return <div>Window width: {width}</div>; }Copy the code

As mentioned above, if you don’t have dependencies, you don’t have to use width, but you can use setWidth, the function is a reference, and the closure variable setWidth is the same address.

Source code analysis

  • useMemoThe encapsulation
function useCallback(callback, args) {
  return useMemo((a)= > callback, args);
}
Copy the code

useRef

use

  • For example, click the button to start the 60-second countdown and click again to stop it.
function Counter() {
  const [start, setStart] = useState(false);
  const [time, setTime] = useState(60);

  useEffect((a)= > { // effect function that does not take or return any arguments
    let interval;
    if (start) {
      interval = setInterval((a)= > {
        setTime(time - 1); // ❌ time is not available in effect closures
      }, 1000);
    }
    return (a)= > clearInterval(interval) // The clean-up function, called when the current component is deregistered
  }, [start]); // The effect function is called when the variables in the array change

  return (
    <button onClick={()= >setStart(! start)}>{time}</button>
  );
}
Copy the code
  • In the previous analysis, because of the closure, get totimeThe value is not up to date. You can usetimeTo the initial value ofuseRef, and then drivetimeThe update.
function Counter() {
  const [start, setStart] = useState(false);
  const [time, setTime] = useState(60);
+ const currentTime = useRef(time); // Generate a mutable referenceUseEffect (() => {// effect function does not accept or return any parameters let interval; if (start) { interval = setInterval(() => {+ setTime(currentTime.current--) // CurrentTime.current is mutable
- setTime(time - 1); // ❌ time is not available in effect closures}, 1000); } return () => clearInterval(interval) // clean-up function, called when the current component is deregistered}, [start]); Return (<button onClick={() => setStart(! start)}>{time}</button> ); }Copy the code
  • UseRef generates an object currentTime = {current: 60}, which remains constant throughout the life of the component.

  • SetTime can be used to replace interval, so that external countdowns can also be cancelled.

function Counter() {
  const [start, setStart] = useState(false);
  const [time, setTime] = useState(60);
- const currentTime = useRef(time); // Generate a mutable reference
+ const interval = useRef() // Interval can be cleared and set anywhere in this scopeUseEffect (() => {// effect takes no arguments and returns no arguments- let interval;
    if (start) {
- interval = setInterval(() => {
+ interval.current = setInterval(() => {
-settime (currentTime.current--) // currentTime.current is mutable
+ setTime(t => t-1) // ✅
      }, 1000);
    }
- return () => clearInterval(interval) // The clean-up function, which is invoked when the current component is deregistered
+ return () => clearInterval(interval.current) // Clean-up function, which is called when the current component is deregistered}, [start]); Return (<button onClick={() => setStart(! start)}>{time}</button> ); }Copy the code

This eliminates repeated creation of interval variables and allows outsiders to clean up the timer interval.current.

Why is that?

  • useRefThe returned object remains unchanged for the lifetime of the component.
  • Why can’t you change the returned object, but only the objectcurrentAttribute?

Source code analysis

function useRef(initialValue) {
  return useMemo((a)= > ({ current: initialValue }), []);
}
Copy the code
  • Internally useduseMemoTo implement, pass in one to generate one withcurrentProperty object function, null array dependent, so the function is executed only once during the entire life cycle.
  • Direct changeuseRefThe value returned cannot be changed internallyhookState._valueValues can only be changed internallyhookState._value.currentTo influence the next use.

useLayoutEffect

use

  • withuseEffectUse the same way.

Why is that?

  • withuseEffectWhat’s the difference?

Source code analysis

  • UseEffect callbacks are executed asynchronously in the option.diffed phase using requestAnimationFrame or setTimeout(callback, 100). Since the authors agree that this is not such a big deal, the code is not posted and there is only one layer of requestAnimationFrame that will not be executed before the next frame.

  • The useLayoutEffect callback is batch synchronized in the option._commit phase.

  • In React, requestIdleCallback or requestAnimationFrame is estimated to be used for time sharding to avoid blocking visual updates.

  • React uses its own internal priority scheduler, which will cause some low-priority tasks to be delayed. You can use useLayoutEffect if you feel that the priority is too high to be synchronized regardless of blocking the render.

useReducer

use

  • Numbers ±1 with reset
const initialState = 0;
const reducer = (state, action) = > {
  switch (action) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'reset': return 0;
    default: throw new Error('Unexpected action'); }};function Counter() {
  const [count, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      {count}
      <button onClick={()= > dispatch('increment')}>+1</button>
      <button onClick={()= > dispatch('decrement')}>-1</button>
      <button onClick={()= > dispatch('reset')}>reset</button>
    </div>
  );
}
Copy the code
  • The second argument can be a function that returnsstateInitial value of;
  • The third argument can be returned by a function that takes the second argument as an argumentstateThe initial value of.

Why is that?

  • When to useuseReducer

The state logic is complex and contains multiple subvalues, and the next state depends on the previous state.

Reducer had better be a pure function, centralized processing logic, modify the source to facilitate traceability, avoid logic scattered, can also avoid unpredictable changes in the state, resulting in difficult bug traceability.

Source code analysis

  • As I said,useStateuseReducerThe implementation.
function useReducer(reducer, initialState, init) {
  const hookState = getHookState(currentIndex++);
  if(! hookState._component) { hookState._component = currentComponent; hookState._value = [ !init ? invokeOrReturn(undefined, initialState) : init(initialState),

      action => {
        const nextValue = reducer(hookState._value[0], action);
        if (hookState._value[0] !== nextValue) {
          hookState._value[0] = nextValue; hookState._component.setState({}); }}]; }return hookState._value;
}
Copy the code
  • The last timestatereducerThe first argument to,dispatchAccepts the second argument and generates a new onestate.

useContext

use

  • For example, set the global themetheme
// App.js
function App() {
  return<Toolbar theme="dark" />; } // toolanto.js function Toolbar(props) {// Theme needs to be layered around all the components. return ( <div> <ThemedButton theme={props.theme} /> </div> ); } // ThemedButton.js class ThemedButton extends React.Component { render() { return <Button theme={this.props.theme} />;  }}Copy the code
  • Use the Context
// context.js
+ const ThemeContext = React.createContext('light');

// App.js
function App() {
- return 
      ;
+ return (
+ 
      
+ 
      
+ 
   );
}

// Toolbar.js
function Toolbar(props) {
  return (
    <div>
- 
      
+ 
       // no passing required
    </div>
  );
}

// ThemedButton.js
class ThemedButton extends React.Component {
+ static contextType = ThemeContext; // Specify contextType to read the current theme context.
  render() {
- return ;
+ return ; // React will find the nearest theme Provider, whose theme value is "dark".}}Copy the code
  • Using useContext
// context.js const ThemeContext = React.createContext('light'); // App.js function App() { return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } // toolanto.js function (props) {return (<div> <ThemedButton /> // no need to pass </div>); } // ThemedButton.js- class ThemedButton extends React.Component {
- static contextType = ThemeContext; // Specify contextType to read the current theme context.
- render() {
- return ; // React will find the nearest theme Provider, whose theme value is "dark".
-}
-}
+ function ThemedButton() {
+ const theme = useContext(ThemeContext);
+ 
+ return ;
+}
Copy the code
  • UseContext (MyContext) is equivalent to static contextType = MyContext in the class component

  • When the most recent <MyContext.Provider> update is made to the component’s upper layer, the Hook triggers a rerender and uses the latest context value passed to MyContext Provider. Even if the ancestor uses React. Memo or shouldComponentUpdate, it will be rerendered when the component itself uses useContext.

You can use React. Memo or useMemo hooks for performance optimization.

Why is that?

  • How does useContext get the context and drive the change?

Source code analysis

  • On the current component, get the context, subscribe to the current component, and publish notifications when the context changes.
function useContext(context) {
  const provider = currentComponent.context[context._id];
  if(! provider)return context._defaultValue;
  const state = getHookState(currentIndex++);
  // This is probably not safe to convert to "!"
  if (state._value == null) {
    state._value = true;
    provider.sub(currentComponent);
  }
  return provider.props.value;
}
Copy the code

Customize the hook

use

  • Common to add anti-shake features to components, such as using ANTDSelectInputComponents, you may use them separately to reassemble a new component, with the anti-shake implementation inside the new component.
  • Custom hook can be used to separate the relationship between components and anti-shake in finer granularity.
/ / if the hooks
function useDebounce() {
  const time = useRef({lastTime: Date.now()});
  return (callback, ms) = > {
    time.current.timer && clearTimeout(time.current.timer);
    time.current.timer = setTimeout((a)= > {
      const now = Date.now();
      console.log(now - time.current.lastTime); time.current.lastTime = now; callback(); }, ms); }}Copy the code
function App() {
  const [val, setVal] = useState();
  const inputChange = useDebounce();
  // Can be used multiple times
  // const selectChange = useDebounce();
  
  return (
    <>
      <input onChange={
        ({target: {value}}) => {
          inputChange(() => setVal(value), 500)
        }
      }/>{val}
    </>
  );
}
Copy the code

Function component hook versus class component

disadvantages

  1. Poor performance, but only at the expense of browser parsing at the JS level.

advantage

  1. Reduce the amount of code, related logic more aggregation, easy to read and maintain;
  2. Don’t understandclassthis.classSo far it’s just syntactic sugar, standards are still changing, there’s no concept like traditional object-oriented polymorphism, multiple inheritance,thisThe cost of understanding is high;
  3. Pure functions are good for exampletsDerivation types, and so on.

reference

  1. The React document
  2. Preact document
  3. Preact source
  4. Refactor your applet using React Hooks