Original: www.netlify.com/blog/2019/0…

Basically, hooks are a relatively simple way to encapsulate state behavior and user behavior. React was the first to use hooks, and has since been widely adopted by other frameworks such as Vue, Svelte, and even in general JS functions. However, hooks functional design requires a deep understanding of javascript closures. Here we reintroduce closures by implementing a simple hooks. There are two main objectives: to ensure the effective use of closures; Shows how to implement an hooks with 29 lines of JS code. Finally, custom hooks are covered.

Tip: You do not need to do these things to understand hooks. These exercises will help you improve your BASIC JS skills. Don’t worry, it’s not too hard.

What is a closure

One selling point of hooks is that they avoid class complexity and higher-order components. But there are those who feel that we are simply substituting one problem for another. We don’t have to worry about context boundaries anymore, but we have to worry about closures. As Mark Dalgleish summed it up so accurately:Closures are a fundamental principle of JS. However, many new JS developers are confused about closures. Kyle Simpson, author of You Don’t Know JS, defines closures this way: Mandatory declaration enables a function to remember and access its lexical scope, even if the function is executed outside the scope. They are clearly linked to the principle of lexical scope, defined in MDN as how a parser resolves variable names when functions are nested. To better understand, let’s take a look at an example:

// Example 0
function useState(initialValue) {
  var _val = initialValue // _val is a local variable created by useState
  function state() {
    // state is an inner function, a closure
    return _val // state() uses _val, declared by parent funciton
  }
  function setState(newVal) {
    // same
    _val = newVal // setting _val without exposing _val
  }
  return [state, setState] // exposing functions for external use
}
var [foo, setFoo] = useState(0) // using array destructuring
console.log(foo()) // logs 0 - the initialValue we gave
setFoo(1) // sets _val inside useState's scope
console.log(foo()) // logs 1 - new initialValue, despite exact same call
Copy the code

Here, we simply implement the React useState hook. The function has two internal functions, state and setState. State returns a local variable, _val, and setState assigns the variable to the parameter passed in (e.g., newVal). So state is an impure getter, so we’re going to modify it a little bit. The important thing is that we can get and control the internal variable _val with foo and setFoo. They can get the scope of useState, which is called a closure. In the context of React or other frameworks, this is state. If you want to dig deeper into closures, I recommend studying the topic articles on MDN, YDKJS, and DailyJS. If you can understand the code above, you’ve mastered closures.

Used in functional components

Let me apply the newly created useState function. We will create a Counter component.

// Example 1
function Counter() {
  const [count, setCount] = useState(0) // same useState as 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

Here, we choose to simply print state instead of rendering to the concrete DOM. We just called Counter manually instead of binding to an event handler. In this way, we can simulate build rendering and response user behavior. Getting state through the getter when the code starts executing is not really a react.usestate hook. Let’s optimize.

The closure before

If we want to match the real React API, our state must be a variable, not a function. But if we simply expose _val, there is a bug:

// Example 0, revisited - this is BUGGY!
function useState(initialValue) {
  var _val = initialValue
  // no state() function
  function setState(newVal) {
    _val = newVal
  }
  return [_val, setState] // directly exposing _val
}
var [foo, setFoo] = useState(0)
console.log(foo) // logs 0 without needing function call
setFoo(1) // sets _val inside useState's scope
console.log(foo) // logs 0 - oops!!
Copy the code

This is a variant of the closure problem we had before. When we reset foo outside of useState, foo points to the _val that useState was initialized with and never changes. This is not what we want. When using variables instead of method calls, we usually want our component state to respond to the current state. The two goals seem to be diametrically opposed.

Closures in modules

We can solve this problem with useState by putting the closure inside another closure.

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

Here we choose to clone a small React in a modular way. React, for example, tracks the state of components. This design allows MyReact to ‘render’ your function components and to set the _val inside each closure execution.

// Example 2 continued
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 React with hooks. _

Copy useEffect

So far, we have implemented the most basic Hook useState in React Hook. The second important hook is useEffect. Unlike useState, useEffect is executed asynchronously, so it is more likely to have closure problems. Let’s extend the previous code.

// Example 3
const MyReact = (function() {
  let _val, _deps // hold our 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]
    }
  }
})()

// usage
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 returns when a dependency changes), we create another variable to track _deps.

We implement useState and useEffect as functions, but unfortunately, both are singletons. In order to do anything pleasingly, we need to create a lot of states and effects. Fortunately, as Rudi Yardley wrote, React Hooks are not magic, they are arrays. Therefore, we need an hooks array. Then place both _val and _deps in the array, because they won’t intersect:

// Example 4
const MyReact = (function() {
  let hooks = [],
    currentHook = 0 // array of hooks, and an iterator!
  return {
    render(Component) {
      const Comp = Component() // run effects
      Comp.render()
      currentHook = 0 // reset for 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++ // done with this hook
    },
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue // type: any
      const setStateHookIndex = currentHook // for setState's closure!
      const setState = newState= > (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook++], setState]
    }
  }
})()
Copy the code

Note the use of setStateHookIndex, which looks like it’s doing nothing, but is used to avoid setState being affected by the currentHook variable. If you notice, setState is problematic because currentHook is an old value.

// Example 4 continued - in usage
function Counter() {
  const [count, setCount] = MyReact.useState(0)
  const [text, setText] = MyReact.useState('foo') // 2nd 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 first instinct is that we need an hooks array, and a pointer that increments as the hooks are called and resets as the component renders. You can see an example of a custom hook.

// 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 really shows why hooks are not magic – whether it’s the React native hooks or the hooks we created earlier, custom hooks can easily become separate hooks.

Understand the principle of Hooks

From the above you can easily understand the first rule of React Hooks: only call Hooks at the top. We also explicitly state that React relies on the currentHook variable in the order in which hooks are called. If you memorize our implementation, you can read through the entirety of the rule of the rule’s explanation and completely understand it. The second rule: only call hooks from functional components. In our implementation this rule is optional, but it is clearly a good practice to explicitly classify code modules that depend on state logic. (As a nice side effect, it’s easy to write tools that guarantee the first rule.)

conclusion

By this point, you have extended your range of capabilities. You might try implementing useRef as a one-liner, or making the render function actually take JSX and mount to the DOM, Or a detailed implementation of React Hooks. But hopefully you’ve deepened your understanding of closures and learned how React Hooks work.