This is the 11th day of my participation in Gwen Challenge

One of the many selling points of Hooks is that they avoid the complexity of classes and higher-order components. Some people, however, feel that Hooks can cause other problems. While we no longer need to worry about the binding context, we now need to worry about closures.

Closures are a basic concept in JS. They are notoriously confusing to many beginner developers. Kyle Simpson’s famous definition of closures in JS You Don’t Know is as follows:

Closures are: when a function executes outside its lexical scope, its lexical scope can still be remembered and accessed.

Closures are obviously closely related to the concept of lexical scope, which is described in MDN as “how the parser resolves the variable names of functions when nested”. Let’s look at a practical example to better illustrate this point:

// Example 0    
function useState(initialValue) {   
    var _val = initialValue //_val is a local variable created by useState
    function state() {  
      // State is an internal function that is also a closure
      return _val // state() uses _val, declared by the parent function
    }   
    function setState(newVal) { 
      // Also an inner function
      _val = newVal // Assign to _val without exposing _val
    }   
    return [state, setState] // Expose both functions to the outside world
  } 
  var [foo, setFoo] = useState(0) // Array destruct is used
  console.log(foo()) // logs 0 - the initial value we give
  setFoo(1) // Assign _val in scope of useState
  console.log(foo()) // logs 1 - The new initial value is obtained despite using the same function call
Copy the code

Here, we built the original version of useState Hook for React. There are two internal functions, state and setState. State returns the local variable _val defined above, and setState assigns the argument passed to it (that is, newVal) to the local variable.

Our state is implemented with getters, which isn’t perfect, but we’ll improve on that. The important thing here is that with foo and setFoo, we can access and manipulate (so called “close”) the internal variable _val. These two functions retain access to the useState scope, and such references are called closures. In the context of React and other frameworks, this looks like a state, and it is.

Use in functional components

Let’s apply our new useState functionality to a common application. Let’s make a counter component

/// Example 1   
function Counter() {    
    const [count, setCount] = useState(0) // Same as useState defined above
    return {    
      click: () = > setCount(count() + 1),   
      render: () = > console.log('render:', { count: count() })  
    }   
  } 
  const C = Counter()   
  C.render() // render: { count: 0 }    
  C.click() 
  C.render() // render: { count: 1 }
Copy the code

Instead of rendering the data to the DOM, we simply output the states in the console. We made the counter provide an external API so that we could run the script directly without having to set it up with a click event handler.

Although this might work, calling the getter function to access state is not the actual way to do it. Let’s improve it.

Closure implementation that cannot update state

If we want to behave like the actual React Hook, the state should be a variable, not a function. If we simply expose _val instead of wrapping it in a function, we get a bug

// Example 0
function useState(initialValue) {   
    var _val = initialValue 
    // Do not use the state() function
    function setState(newVal) { 
      _val = newVal 
    }   
    return [_val, setState] // direct exposure _val
  } 
  var [foo, setFoo] = useState(0)   
  console.log(foo) // logs 0 does not require a function call
  setFoo(1) // Assign _val in the useState scope
  console.log(foo) // logs 0 - oops!!
Copy the code

This is a problem where closures cannot be updated. When we deconstruct the foo variable from the output of useState, the value of foo is equal to the _val value of the initial call to useState, and it doesn’t change after that! This is not what we want, usually we want the component state to reflect the current state, and the state should be a variable, not a function! The two goals seem incompatible.

Closure implementation of the module pattern

We can solve this useState conundrum… By putting the closure inside another closure!

// Example 2    
const MyReact = (function() {   
    let _val // Keep our state in module scope
    return {    
      render(Component) {   
        const Comp = Component()    
        Comp.render()   
        return Comp 
      },    
      useState(initialValue) {  
        _val = _val || initialValue // Reassign each run
        function setState(newVal) { 
          _val = newVal 
        }   
        return [_val, setState] 
      } 
    }   
  })()
Copy the code

Here we chose to use module mode to make our tiny React Hook. Like React, it can record the state of components (in this case, it can only record one state per component, logging the state in val). This design allows MyReact to render your functional components, which can use its corresponding closure each time the component is updated, assigning the value to the inner val.

// 续Example 2   
function Counter() {    
    const [count, setCount] = MyReact.useState(0)   
    return {    
      click: () = > setCount(count + 1), 
      render: () = > console.log('render:', { count })   
    }   
  } 
  let App   
  App = MyReact.render(Counter) // render: { count: 0 } 
  App.click()   
  App = MyReact.render(Counter) // render: { count: 1 }
Copy the code

It now looks more like Hooks from React.

So far, we’ve implemented useState, which is the most basic React Hook. The next important Hook is the useEffect. Unlike setState, useEffect is executed asynchronously, which means closure issues are more likely to occur.

We can extend the React model to include the following code:


// Example 3    
const MyReact = (function() {   
    let _val, _deps // Preserve state and dependencies in scope
    return {    
      render(Component) {   
        const Comp = Component()    
        Comp.render()   
        return Comp 
      },    
      useEffect(callback, depArray) {   
        consthasNoDeps = ! depArrayconsthasChangedDeps = _deps ? ! depArray.every((el, i) = > el === _deps[i]) : true   
        if (hasNoDeps || hasChangedDeps) {  
          callback()    
          _deps = depArray  
        }   
      },    
      useState(initialValue) {  
        _val = _val || initialValue 
        function setState(newVal) { 
          _val = newVal 
        }   
        return [_val, setState] 
      } 
    }   
  })()  
  // How to use it
  function Counter() {  
    const [count, setCount] = MyReact.useState(0)   
    MyReact.useEffect(() = > {   
      console.log('effect', count)  
    }, [count]) 
    return {    
      click: () = > setCount(count + 1), 
      noop: () = > setCount(count),  
      render: () = > console.log('render', { count })    
    }   
  } 
  let App   
  App = MyReact.render(Counter) 
  // effect 0   
  // render {count: 0}  
  App.click()   
  App = MyReact.render(Counter) 
  // effect 1   
  // render {count: 1}  
  App.noop()    
  App = MyReact.render(Counter) 
  // // no effect run   
  // render {count: 1}  
  App.click()   
  App = MyReact.render(Counter) 
  // effect 2   
  // render {count: 2}
Copy the code

To track dependencies (because useEffect reruns callback only if the dependency changes), we introduce another variable, _deps.

No magic, just an array

We have copied the functionality of useState and useEffect pretty well, but they are poorly implemented singletons (allowing only one state, one side effect, and more bugs). To make things more interesting, we need to extend it to accept any number of states and side effects. Fortunately, as Rudi Yardley has written, React Hooks aren’t magic, they’re just arrays. So we use an hooks array. We put val and DEps all in the same array because they don’t interfere with each other.

// Example 4    
const MyReact = (function() {   
    let hooks = [], 
      currentHook = 0 // hooks array, and an iterator!
    return {    
      render(Component) {   
        const Comp = Component() / / run the effects
        Comp.render()   
        currentHook = 0 // Reset for the next render
        return Comp 
      },    
      useEffect(callback, depArray) {   
        consthasNoDeps = ! depArrayconst deps = hooks[currentHook] // type: array | undefined  
        consthasChangedDeps = deps ? ! depArray.every((el, i) = > el === deps[i]) : true 
        if (hasNoDeps || hasChangedDeps) {  
          callback()    
          hooks[currentHook] = depArray 
        }   
        currentHook++ // This hook is finished
      },    
      useState(initialValue) {  
        hooks[currentHook] = hooks[currentHook] || initialValue // type: any    
        const setStateHookIndex = currentHook // A variable for the closure of setState!
        const setState = newState= > (hooks[setStateHookIndex] = newState)  
        return [hooks[currentHook++], setState] 
      } 
    }   
  })()
Copy the code
// Example 4
function Counter() {    
    const [count, setCount] = MyReact.useState(0)   
    const [text, setText] = MyReact.useState('foo') // Second state hook!
    MyReact.useEffect(() = > {   
      console.log('effect', count, text)    
    }, [count, text])   
    return {    
      click: () = > setCount(count + 1), 
      type: txt= > setText(txt),    
      noop: () = > setCount(count),  
      render: () = > console.log('render', { count, text })  
    }   
  } 
  let App   
  App = MyReact.render(Counter) 
  // effect 0 foo   
  // render {count: 0, text: 'foo'} 
  App.click()   
  App = MyReact.render(Counter) 
  // effect 1 foo   
  // render {count: 1, text: 'foo'} 
  App.type('bar')   
  App = MyReact.render(Counter) 
  // effect 1 bar   
  // render {count: 1, text: 'bar'} 
  App.noop()    
  App = MyReact.render(Counter) 
  // // no effect run   
  // render {count: 1, text: 'bar'} 
  App.click()   
  App = MyReact.render(Counter) 
  // effect 2 bar   
  // render {count: 2, text: 'bar'}
Copy the code

Therefore, the basic idea is to use arrays to store hook states and dependencies, and only need to increase the index number to operate the corresponding array item to call each hook, and reset the index after the component render is finished.

It is also easy to implement custom hooks:


// Example 4, revisited 
function Component() {  
    const [text, setText] = useSplitURL('www.netlify.com')  
    return {    
      type: txt= > setText(txt),    
      render: () = > console.log({ text })   
    }   
  } 
  function useSplitURL(str) {   
    const [text, setText] = MyReact.useState(str)   
    const masked = text.split('. ')  
    return [masked, setText]    
  } 
  let App   
  App = MyReact.render(Component)   
  // { text: [ 'www', 'netlify', 'com' ] }  
  App.type('www.reactjs.org')   
  App = MyReact.render(Component)   
  // { text: [ 'www', 'reactjs', 'org' ] }}
Copy the code

This is how hooks work in practice. Custom hooks simply use the primitives provided by the framework, whether it’s in React or the mini-hooks we’ve made here.

Use the law of hook

Now you can easily understand the first rule of using Hooks: call Hooks only at the top level. Because we use the currentHook variable, we need to model the React dependency based on the call order. You can read the Hooks law interpretation against our code implementation and understand everything.

The second rule, “Only call Hooks in the React function”. With our implementation approach, this rule is not mandatory, but it is good practice to clearly define which parts of the code depend on stateful logic. (This also makes it easier to write tools to ensure compliance with the first rule. You don’t inadvertently wrap stateful functions in loops and conditional statements as normal functions. Obeying the second law helps obeying the first.)

conclusion

At this point, we have extended the original example far enough. You can try to implement useRef with one line of code, or have the Render function accept JSX and mount it to the DOM, or implement countless other important details that we’ve omitted in the 28 lines of the Hook version. Hopefully now you have gained some experience using closures in context and have a useful model in mind that explains how React Hooks work.