Author: Xiao Asked

Why Hooks API?

Hooks API has been available to developers for nearly two years since 2018, but there is still a lot of confusion about Hooks.

  1. Why switch from Class API to Hooks API?

  2. The Hooks API uses methods that are very different from the Class API and need to be relearned.

  3. The large amount of function nesting makes “closure traps” very common. It is not hard to see that there are already many tutorials on the Hooks API, and even analysis of the source code. But when we actually look at the React source code to learn how to implement the Hooks API, we get even more confused by the “magic” implementation. So how else can we learn and understand the Hooks API design pattern?

TL; DR

This article contains the following:

  • The Hooks API differs from the Class API

  • Functional programming in the Hooks API

  • Hooks API and revealed the/Algebra

  • React Hooks with Vue Composition API

YES

This article will help you understand the Hooks API better by exploring it from another perspective.

NO

This article does not teach you how to use the Hooks API, nor does it directly help you to use the Hooks API better.

Yeah, you want to read more? Please don’t skip any sentences, or it will seriously affect the reading experience.

Functional programming

In fact, the core of the Hooks API is the development pattern of Functional Programming, with most of the API representation provided in the form of functions. Functional programming has been around since the beginning of the React framework.

View=Render(State)

This is a key key to understanding the React Immutable development mode. JSX is a function used by React to convert State into a Virtual DOM. Developers use JSX to define the conversion relationship between State -> View, and then use setState to change the component State. React transforms and combines all the states using the Render function of each component to get the actual page structure.

In the React development ecosystem, after several iterations of the state management tool, Redux, also based on functional programming, became the most popular choice in the React development community. In Redux’s design pattern, the functional concept is even more thoroughly embodied. The Reducer needs to be triggered by Action to change the State in the Redux Store. The core basic concept of Reducer is:

State’ = Reducer(State, Action)

interface State {
  count: number
}
const state: State = {
  count: 0
}

const INCR = (state: State, payload: number = 1): State => {
  return {
    count: state.count + payload
  }
}
const DECR = (state: State, payload: number = 1): State => {
  return {
    count: state.count - payload
  }
}
Copy the code

Redux state management is actually achieved by “logging” results using continuous Reducer+ actions, and we can see a list of actions executed in chronological order in Redux Devtools, and even “time travel” through the Action queue. However, the actual development mode is relatively high for students who are not familiar with functional programming or have general mathematical skills.

Let’s return to the functional programming of the Hooks API itself. Even though React and Redux already support the functional programming paradigm, why do stateful components have to be developed in object-oriented form? In fact, React Class API development mode is based on the state machine mode. Components change the this.state in the component instance to make the component view respond. Of course, there are no problems with this pattern in component development, but problems with Class apis arise when you need to abstract and reuse some of the logic in a component.

Suppose you have counter logic that needs to be abstracted. In the Class API, there are two abstractions: Mixin and HOC (Higher Order Component).

Logical abstraction mode

Mixins

React.createclass is a very “crude” way of reusing logic by mixing abstract logic into business components in the manner of react.createclass. But this mode is very restrictive and prone to conflicts.

import React from "react" import ReactDOM from "react-dom" import { Row, Col, Button } from "antd" const counterMixin = { getInitialState() { return { count: 0 } }, incr(increment = 1) { this.setState({ count: this.state.count + increment }) }, decr(decrement = 1) { this.setState({ count: This.state.count-decrement})}} // Deprecated since React 15.5.0 const Counter = reace.createclass ({mixins: [counterMixin], render() { return ( <Row style={{ width: 150, textAlign: "center" }}> <Col span={24}> <h3>{this.state.count}</h3> </Col> <Col span={12}> <Button onClick={() => this.incr()}>INCR</Button> </Col> <Col span={12}> <Button onClick={() => this.decr()}>DECR</Button> </Col> </Row> ) } })Copy the code

DEMO: codesandbox. IO/s/counter – w…

HOC

CreateClass has been removed since React 15.5.0, and HOC is a more flexible extension. The core logic of HOC is to wrap business components and pass encapsulated logic to business components in the form of parameters.

import React, { Component } from "react" import ReactDOM from "react-dom" import { Row, Col, Button } from "antd" function CounterComponent(WrappedComponent) { return class extends Component { state = { count: 0 } incr(increment = 1) { this.setState({ count: this.state.count + increment }) } decr(decrement = 1) { this.setState({ count: this.state.count - decrement }) } render() { const countProps = { count: this.state.count, incr: (increment) => this.incr(increment), decr: (decrement) => this.decr(decrement) } return <WrappedComponent {... this.props} {... countProps} /> } } } const Counter = CounterComponent((props) => { const { count, incr, decr } = props return ( <Row style={{ width: 150, textAlign: "center" }}> <Col span={24}> <h3>{count}</h3> </Col> <Col span={12}> <Button onClick={() => incr && incr()}>INCR</Button> </Col> <Col span={12}> <Button onClick={() => decr && decr()}>DECR</Button> </Col> </Row> ) })Copy the code

DEMO: codesandbox. IO/s/counter – w…

This approach may seem simple, but it can be cumbersome if you need to add state to a business component. If you need to pass in more complex component logic, you’ll need to use methods like CONNECT.

How does this need to be implemented in the Hooks API?

Hooks API

The React of the Hooks API enabled functionality components that were meant to be Stateful as Stateless Component, and Hooks themselves were implemented via functions.

import React, { useState } from "react" import ReactDOM from "react-dom" import { Row, Col, Button } from "antd" function useCount(initCount = 0) { const [count, setCount] = useState(initCount) const incr = () => setCount(count + 1) const decr = () => setCount(count - 1) return { count, incr, decr } } function Counter() { const { count, incr, decr } = useCount() return ( <Row style={{ width: 150, textAlign: "center" }}> <Col span={24}> <h3>{count}</h3> </Col> <Col span={12}> <Button onClick={() => incr()}>INCR</Button> </Col>  <Col span={12}> <Button onClick={() => decr()}>DECR</Button> </Col> </Row> ) }Copy the code

DEMO: codesandbox. IO/s/counter – w…

Well, it looks much easier. But something strange happens when we read the modified state as if we were using the Class API + Async/Await. For example, if we need to print a new count after executing incr, in the Class API we can get the correct new value later in the code by executing asynchronously with await while executing this.setState. In the Hooks API, the correct value cannot be obtained even with Async/Await.

import React, { useState } from "react" import ReactDOM from "react-dom" import { Row, Col, Button, Collapse, Input } from "antd" const { TextArea } = Input const { Panel } = Collapse class CounterA extends React.Component { state  = { count: 0 } async incr(increment = 1) { await this.setState({ count: this.state.count + increment }) } async incrAndLog(increment = 1) { await this.incr(increment) this.props.log("Class API", this.state.count) // Here will log the new value } decr(decrement = 1) { this.setState({ count: this.state.count - decrement }) } render() { return ( <Row style={{ width: 300, textAlign: "center" }}> <Col span={24}> <h3>{this.state.count}</h3> </Col> <Col span={8}> <Button onClick={() => this.incr()}>INCR</Button> </Col> <Col span={8}> <Button onClick={() => this.incrAndLog()}>INCR&LOG</Button> </Col> <Col  span={8}> <Button onClick={() => this.decr()}>DECR</Button> </Col> </Row> ) } } function useCount(initCount = 0) { const [count, setCount] = useState(initCount) const incr = async () => await setCount(count + 1) const decr = async () => await setCount(count - 1) return { count, incr, decr } } function CounterB({ log }) { const { count, incr, decr } = useCount() const incrAndLog = async (increment = 1) => { await incr(increment) log("Hooks API", count) // Here will log the previous value, WHY? } return ( <Row style={{ width: 300, textAlign: "center" }}> <Col span={24}> <h3>{count}</h3> </Col> <Col span={8}> <Button onClick={() => incr()}>INCR</Button> </Col> <Col span={8}> <Button onClick={() => incrAndLog()}>INCR&LOG</Button> </Col> <Col span={8}> <Button onClick={() => decr()}>DECR</Button> </Col> </Row> ) } function App() { const [logText, setLogText] = useState("[LOGGING HERE]") const log = (domain, msg) => { setLogText(logText + "\n" + `[${domain}] ${msg}`) } return ( <Row gutter={15}> <Col span={12}> <h4>DEMO</h4> <Collapse defaultActiveKey={[1]}> <Panel header="Counter with Class API" key={1}> <CounterA log={log} /> </Panel> <Panel  header="Counter with Hooks API" key={2}> <CounterB log={log} /> </Panel> </Collapse> </Col> <Col span={12}> <h4>Log</h4> <TextArea rows={8} value={logText} /> </Col> </Row> ) }Copy the code

DEMO: codesandbox. IO/s/qiguaide -…

Does it feel like while the Hooks API makes code look cleaner when used, it adds a lot of mental cost to the quirks? Yes, I quite agree with that, the Hooks API introduces a lot of abstractions and obscurity, making for a steep initial learning curve.

But don’t be afraid, let me take a slow look at how TypeScript can help you understand and learn functional programming and Hooks apis.

Quick hands-on functional programming based on TS type derivation

Basic types of

First, let’s look at two basic function types:

  • F\text{<}T\text{>} \rightarrow T, function F argument type is T, this function return value type is T;

  • F\text{<}T\text{>} \rightarrow U, F\text{<}T\text{>} \rightarrow U, F\text{<}T\text{>} \rightarrow U.

A simple explanation is that the first is that the outgoing and incoming parameters of a function are of the same type; The second is that the input parameter of the function after the execution of the function, the type of the input parameter is inconsistent with that of the input parameter, which can be understood as a mapping.

Type inference

Now that we have these two basic types, we can start morphing them. But before that, let’s take a look at two words from the industry:

I don’t know the data, are parameters;

I don’t know the behavior, it’s all parameters;

These two statements cover more than 90% of functional programming scenarios in the Web development context, so you can see what they mean in the following series of functional variations.

  • F\text{<}T\text{>} \rightarrow T

    • F\text{<}F\text{<}T\text{>}{>} \rightarrow F\text{<}T\text{>}, a function F\text{<}T\text{>} as the input and output type of another function F, see example: useCallback
  • F\text{<}T\text{>} \rightarrow U

    • F\text{<}F\text{<}T\text{>}{>} \rightarrow T, unlike the above, the return value is the parameter T of F\text{<}T\text{>}, see useMemo

    • F \ text {<} F \ \ text text {<} T {>} {>} \ rightarrow (F \ text {<} T \ text {>}, U), pass in a function F \ text {<} T \ text {>}, return in addition to the function itself, also return to some other state U, Example: useRequest

    • F\text{<}T{>} \rightarrow (T, F\text{<}T\text{>}), passing in a type T, returns a function F\text{<}T\text{>} in addition to T itself, for example: useState

The numbers I don’t know are parameters

This sentence is easy to understand, referring to Lodash, the Swiss Army Knife most popular in the JavaScript development community, In this way, you can think of F\text{<}T\text{>} \rightarrow U or F\text{<}T,F\text{<}T\text{>} \rightarrow U\text{>} \rightarrow U\text{>} \rightarrow U\text{>} \rightarrow The representation of U.

In a function, I don’t know what data I’m dealing with, or even the exact type, but the logic I need to execute is certain.

DEMO: codesandbox. IO/s/lodash – da…

The behavior I don’t know, it’s all parameters

In Web development, we often need to control some custom behavior in business logic, such as:

  • To send an Ajax request, you need to know the following: useRequest

    • Whether the request is waiting to return: Loading status

    • Success of the request returns: Success status

    • Whether the request needs to be triggered manually: Trigger condition

    • If the request is triggered automatically, the triggering conditions are as follows: Dependent refresh

    • Whether the request needs to be cached: cache condition, cache status

  • There are a series of behaviors involving asynchronous operations that need to be automatically queued and executed serially in order to reduce the amount of concurrency on the system: runTrack

    • Where behavior is defined, we want to be insensible

    • The executor also does not need to be aware of the queue

Taking the first Ajax request scenario as an example, we want a tool that wraps the actual business behavior and uses the wrapped outgoing parameter in the same way as the behavior used as the input parameter.

const { run: fetchPosts, loading } = wrapRequest((authorId: number) => fetchPostsService(authorId))

fetchPosts(123)

// ...

const statusText = loading ? "Loading..." : "Done"
Copy the code

In fact, we have a very familiar “I don’t know behavior” scenario, which is the Compose and Render process of components, and then we need to introduce another concept, codata.

It’s not that esoteric PLT/math concept, codata

Fear not, CoData is actually nothing new. When we learned how to solve equations in elementary school math, we began to be exposed to a concept called Algebra. What? Algebra is too abstract to be a programming analogy, right? It’s okay. We’ll take it one step at a time.

S = PI r ^ 2

Remember the formula for calculating the area of a circle: S=\ PI r^2, where:

  • S is the area of the circle, which is what this formula is looking for;

  • \ PI is PI, as a common irrational number, it will have different precision approximations in different scenes. For example, we generally use 3.14 as an approximation for manual calculation when learning, and in scenes with higher precision requirements may need to use N decimal places;

    • In fact, however, we should think of \ PI as the result of a PI calculation algorithm with different approximations
  • R is the radius of the circle, and r itself is an unknown quantity.

In such a simple formula, including the formula itself, there are already three algebras. In mathematics, we can think of unknowns and algorithms as algebra, which has no specific values, but we know how to use them.

A little abstract? It doesn’t matter that we use the example in the context of Web development, assuming the following PRDS need to be implemented:

In this PRD, the following elements are included:

  1. A slider that changes the radius r of the circle

  2. A checkbox that can change the approximate value used by PI \ PI

  3. A circle that displays its size according to the selected radius

  4. A string showing the area of a circle

To implement elements in PRD using actual React Hooks, you need to define the following (pseudocode) :

import { useState, useMemo } from 'react'

function App() {
  const [r, setR] = useState(10)
  const [PI, setPI] = useState(3.14)
  
  const S = useMemo(() => PI * Math.pow(r, 2), [PI, r])
  const circleCSS = useMemo(() => ({
    width: r * 2,
    height: r * 2,
  }), [r])
}
Copy the code

In this code, four entities are defined that correspond to the four elements of the above requirement, which are sufficient for all the read-only parts of the PRD development. But in PRD, the two most important elements of the circle, the radius r and PI \ PI, can be changed, so the actual values of these four elements are unknown until the actual calculation. Only when the component is actually rendered does the React framework actually read the current user-selected, time-sensitive value from memory for calculation.

Compose & Visit

Well, do you feel anything? We can divide the process of using CoData into two parts: Compose and Visit. Assuming that we also express this function component in mathematical language, it could look something like this:

Render = F\text{<}\pi,r,S,\text{circleCSS}\text{>}

CircleCSS and S can be rewritten as F\text{<}\ PI,r\text{>} and F\text{<}r\text{>}, so the rendering function can be written as follows:

Render = F\text{<}\pi,r,F\text{<}\pi,r\text{>},F\text{<}r\text{>}\text{>}

Doesn’t that make it easier? This rendering function is actually a binary function of radius r and PI \ PI, and in the Web development world it is possible to take partial derivatives of these two elements respectively. What’s a partial derivative? It doesn’t matter. The simple understanding is that any change in these two elements can cause a predictable change in the overall component. Do you recall anything? Yeah, that’s Redux’s idea.

DEMO: codesandbox. IO/s/yuandemia…

We build Render = F \ text rendering function {<} \ PI, r, F \ text {<} \ PI, r \ text {>}, F \ text {<} r \ text {>} \ text {>} process is actually a process of combination, Each unknown element in this process does not actually use its actual value, but only its concept itself. Only when the function is actually observed, i.e. rendered, will the actual values inside the function be observed and used to calculate the values of each layer of function to get the actual result of the render function.

When you reuse logic by writing custom Hooks, you are actually taking those custom Hooks as parameters in the business component, it is a composite process.

The advantage is that components are inherently more Lazy Compute friendly and have more room for performance tuning. In React Hooks, useMemo and useCallback are also provided to reduce unnecessary calculations during observations and further optimize performance.

The harsh truth is that most components can’t be written as differentiable functions perfectly, because we often need Side Effects to implement our more complex requirements. UseEffect is also provided in React Hooks.

Returning to the function type definition itself, the render function is actually a variant of F\text{<}T\text{>} \rightarrow U, so the type T representing the render function parameter must be determined during the composition process in order to obtain the determined U, which is the observed result of the render interface. This also explains why the Hooks API does not allow the use of the Hooks API in logical branches, nor in subfunctions, because both of these cases can result in an unstable parameter T during composition, resulting in an uncertain result type U that does not match the expected result.

Note that T does not refer to any particular parameter, but to the entire parameter combination. Changes in the number of parameters, changes in the order of the parameters indicate that this T has changed, or it is no longer this T.

Of course, Codata has more features than Algebra, but we will not expand it here, and a separate paper may be written in the future to elaborate on this familiar yet unfamiliar concept.

React Hooks with Vue Composition API

We know that after the first release of the Hooks API on React, another popular Web framework, Vue, also released version 3.0. The biggest difference between Vue 3.0 and 2.x is that Vue 3.0 also introduced the Hooks API, which has a different name — Composition API.

Compared to the function components of React Hooks, the Vue Composition API retains the unique legacy advantage of Vue — the Single File Component. Vue still retains a much more primitive and manageable template engine than React, which uses JSX as a representation of the view layer.

import Vue from "vue" import VueCompositionAPI, { ref } from "@vue/composition-api" import Antd from "ant-design-vue" Vue.use(VueCompositionAPI) Vue.use(Antd) const template = ` <a-row id="app"> <a-col :span="24"> <h3>{{count}}</h3> </a-col> <a-col :span="12"> <a-button @click="() => incr()">INCR</a-button> </a-col> <a-col :span="12"> <a-button @click="() => decr()">DECR</a-button> </a-col> </a-row> ` new Vue({ name: "App", template, setup(props) { const count = ref(0) const incr = (increment = 1) => (count.value += increment) const decr = (decrement =  1) => (count.value -= decrement) return { count, incr, decr } } }).$mount("#app")Copy the code

DEMO: codesandbox. IO/s/vue – compo…

It is not hard to find that the same process of Composition and observation is divided into two parts corresponding to the setup method and template rendering in Vue Composition API (of course, it can also be jSX-based render function). This is much closer to the coData concept than React Hooks. And from an implementation standpoint, the Compose process of the Vue Composition API is executed only once, which is much more manageable and easier to tune for performance than the React Hooks’ Compose process, which reexecutes the Compose every time any parameters change.

In addition, Vue Composition API is designed to be more friendly to developers who already use Vue 2.x. F\text{<}T\text{>} \rightarrow (T, The model of F\text{<}T\text{>}) is compressed into F\text{<}T\text{>} rightarrow Ref\text{<}T\text{>}, which takes most of the complex mental cost inside the framework and significantly lowers the bar to entry for the Hooks API.

Play together?

Even now, there is a project antFU/Reactivue that integrates the two, which is a fun idea.

import React from "react" import { defineComponent, ref } from "reactivue" import { Row, Col, Button } from "antd" const Counter = defineComponent( // Setup () => { const count = ref(0) const incr = (increment = 1)  => (count.value += increment) const decr = (decrement = 1) => (count.value -= decrement) return { count, incr, decr } }, // Render ({ count, incr, decr }: any) => ( <Row style={{ width: 150, textAlign: "center" }} className="App"> <Col span={24}> <h3>{count}</h3> </Col> <Col span={12}> <Button onClick={() => incr()}>INCR</Button> </Col> <Col span={12}> <Button onClick={() => decr()}>DECR</Button> </Col> </Row> ) )Copy the code

DEMO: codesandbox. IO/s/blue – fast…

The last

Thank you for taking the time to read an article without a tutorial. If this article brings you some new ideas, rethinking the Hooks API and React application development would be a great achievement.

When learning a new technology, it is often tempting to “learn deeply” by reading the source code, but sometimes the source code is less important than the design ideas. Hooks apis are a great example of this. The fact that Hooks are implemented in such a way that learning Hooks only from the source code point of view only leads to long periods when they cannot get past their initial learning slope and lose interest in them.

I would appreciate your comments.