“This is the third day of my participation in the Gwen Challenge in November. Check out the details: The last Gwen Challenge in 2021.”

In this article, we reintroduce closures by building React Hooks. This will serve two purposes — to show the effective use of closures, and to show how Hooks can be built with just 29 lines of readable JS. Finally, we saw how custom Hooks come about. You don’t need to do any of this to understand Hooks. If you complete this exercise, it will probably only help you with your JS basics. Don’t worry, it’s not that hard!

What is a closure?

Closures are a basic concept in JS. Nonetheless, they are notorious for confusing many, especially new developers. Kyle Simpson of You Don’t Know JS defines closures as:

A closure is when a function can remember and access its lexical scope, even if the function is executed outside of its lexical scope.

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

// Example 0
function useState(initialValue) {
  var _val = initialValue
  function state() {
    return _val
  }
  function setState(newVal) {
    _val = newVal
  }
  return [state, setState]
}
var [foo, setFoo] = useState(0)
console.log(foo())
setFoo(1)
console.log(foo())
Copy the code

Here, we are creating the original clone of the ReactuseState hook. In our function, there are two internal functions, state and setState. State returns a local variable defined above _val and sets the local variable setState to the argument passed to it (that is, newVal).

Our implementation here in state is a getter function, which is not ideal, but we’ll fix it later. Importantly, with fooand setFoo, we can access and manipulate (aka “close”) the internal variable _val. They retain access to the scope of useState, which is referred to as a closure. In the context of React and other frameworks, this looks like state, which is exactly what it is.

Use in function components

Let us useState apply our newly created clone in a familiar environment. We will make a Counter component!

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

In this case, instead of rendering to DOM, we chose console.log to exit our state. We also exposed a programming API for our Counter, so we could run it in a script instead of attaching an event handler. With this design, we were able to simulate our component rendering and reactions to user actions.

While this works, calling the getter to access the state is not really an API for the React.usestate hook. Let’s solve this problem.

Closures in modules

We can use estate through… Move our closure to another closure to solve our problem!

// Example 2
const MyReact = (function() {
  let _val
  return {
    render(Component) {
      const Comp = Component()
      Comp.render()
      return Comp
    },
    useState(initialValue) {
      _val = _val || initialValue
      function setState(newVal) {
        _val = newVal
      }
      return [_val, setState]
    }
  }
})()
Copy the code

Now this looks more like React with Hooks!

copyuseEffect

So far, we’ve introduced useState, which is the first basic React Hook. The next most important hook is useEffect. With different setStates, useEffect is executed asynchronously, which means more opportunities for closure problems.

We can extend the React minimodel we’ve built so far to include the following:

// Example 3
const MyReact = (function() {
  let _val, _deps
  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 reruns when dependencies change), we introduce another variable to track _deps.

So the basic intuition is to have an array of hooks and an index that are incremented as each hook is called and reset when the component renders.

// 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

Derive the rules for Hooks

Note that from here you can easily understand the first rule of Hooks: call Hooks only at the top level. We’ve explicitly modeled the React dependence on the call order with our currentHook variable. You can read through the full interpretation of the rules with our implementation in mind and fully understand what is happening.

Also note that the second rule, “only call the hooks from the React function,” is not a necessary consequence of our implementation either, but it’s certainly a good habit to make it clear which parts of the code depend on stateful logic. (As a nice side effect, it also makes it easier to write tools to ensure that you follow the first rule. By wrapping stateful functions named regular JavaScript functions inside loops and conditions. Rule 2 below helps you follow Rule 1.