Can React Hooks be implemented in a similar way to the Vue 3 Composition API instead?
No, because it’s a very different mental model. I think many students only pay attention to the similarities between the two sets of APIs in terms of functionality, and underestimate the differences in the “big picture” of the two frameworks.
Before I begin the text, let me state that
-
First, the views expressed in this article do not represent the company. I felt that there were too many voices in the circle that disagreed with Hooks (no offense), so I volunteered to come out and balance it.
-
The second is that I really haven’t actually written the front-end for a long time, React Hooks is not much practice, and Vue 3 only skimmed the Composition API RFC and the previous Chinese Vue Function-based API RFC (so I’m not too familiar with the details.) You are welcome to correct and supplement (and ask for additions).
Introduction
“Frameworks/libraries are abstractions of programming languages” does not mean that frameworks cannot be designed without the idioms and programming paradigms of the language in which they are implemented.
Thanks to several very Lisp features of JavaScript: first-class citizen functions, dynamic typing, and some macro support (such as Babel), the development of front-end frameworks in recent years can see many programming language design ideas: frameworks have become syntax composed of DSLs and APIs, semantics are discarded from JavaScript and attached by APIs, The combination of the runtime that underpins the operation of the system and the mental model it expresses.
Vue 3, “Reactive (Closure-based) OOP”
Let’s start with Vue (Vue in this article mainly refers to Vue using the Composition API)
const Counter = {
setup(initialProps) {
const count = reactive({count: 0}) // or `ref(0)`
const inc = () => { count.value++ }
return {count, inc}
}
template: "..."
}
The semantics chosen by Vue for components is “object”: the composition API’s setup will only be called once and return an object to be merged into the Counter component. This object and its members are all persistent references, including the state count stored in the inc closure. lasting. Rendering is the materialization of the template field on the component.
The additional core semantics of Vue is “reactive” (based on mutable data): the state count is a reactive object, the way inc changes the state is to directly modify the count, and the result of the state change is to execute all observers (watcher) logic, including re-rendering and performing side effects (watchEffect), are incorporated into the data flow based on this semantics.
Some students (such as the questioner) said that if you change it to return a render function and directly use closures to save component variables, do you still think this is the semantics of the object?
return (props) => <a onClick={inc}>{count.value}</a>
Yes. The Vue implementation needs to keep the function and its closure reference unchanged (referential identity) to satisfy the semantics of state being persisted, which is the classic JavaScript pattern of using closures to emulate the private properties of objects. (“The closure is the object of the poor, and the object is the closure of the poor”)
To help you understand, what if we had a hypothetical Vue language……
// hypothetical Vue lang
component Counter (props) { // constructor
@reactive count = 0 // field
inc() { count.value ++ } // method
render() { return <a onClick={inc}>{count.value}</a> }
}
Isn’t there a flavor to class-based OOP? Except that Vue’s objects are implemented on a singleton (or closure) rather than a class (or prototype), and the members are reactive! Java).
The core of the Vue runtime is dependency tracking, first of all, it makes the reactive semantics relatively implicit for the user, and the dependencies are automatically collected, which greatly reduces the mental burden on the user. Secondly, it has very fine tracking granularity, and with Vue’s use of a relatively high degree of static, templates make re-rendering automatic and very accurate.
To sum up, Vue’s mental model in terms of components is still “objects that have data and behavior and are self-responsive”. As long as you think along this line of thinking, it will be easier to understand “why Vue’s state can use variable data structures”. “Why Vue needs ref to wrap value types”, and what RFC mentioned when comparing React Hooks, “Why Vue is closer to the JS everyone is used to” (this is more subjective), “Why Vue’s GC pressure will be less”, “Why does Vue not need to manually declare dependencies” and other advantages come from.
React, “Purely (Semi-Monadic/Algebraic) FP”
Let’s look at React (React in this article mainly refers to React under the Hooks API)
function Counter(props) {
const [count, setCount] = React.useState(0);
const inc = () => setCount(count + 1)
return <a onClick={inc}>{count}</a>
}
The semantics React chooses for components is “function”, and each rendering is a real call to the Counter function. Each time useState is executed and the current state is taken from React to count, a new inc function is created each time (so the new count value is captured in its closure).
The additional core semantics of React is an “evaluation context” with controlled side effects. In layman’s terms, it is the running environment of React: the state count must be taken out from the React context every time, and the way inc changes the state is to use setCount. Update the content in the context. As a result of the state change, this function will be called again. When called, the function will obtain the new state from the new context, re-render and schedule context-controlled side effects (useEffect).
To help you understand, what if we had a hypothetical React language……
// hypothetical React lang Ⅰ
component Counter = (props) => // function
@context.state(1) { // context provides `get_or` and `put`
count <- get_or(0) // get from context (or use default value)
let inc = () => put(count + 1) // callback can update the context
return <a onClick={inc}>{count}</a>
}
Isn’t there a hint of Monad-based Haskell? It’s just that React makes the API completely self-sufficient without you having to figure out the complexity. If you’re not familiar with the concept of Monad as a pure FP, we can use it as the “context” of the text without being rigorous. The reason M-bomb is thrown is that it’s often used as a benchmark for dealing with side effects in pure FP, and to help us show how to reduce React to pure FP.
Some students will wonder, how is this different from the “pure function” I recognize, is this also “pure functional programming”?
component Counter = (props) =>
context.state(1).get_or(0).then([count, put] => { // `then` is monadic bind.
let inc = () => put(count + 1)
return <a onClock={inc}>{count}</a>
}).unwrap() // assuming it's safe.
Ever thought of a promise with an asynchronous context, also called Monad?
To (over)simplify, you can think of it as the most straightforward state-passing style (in fact, the React team considered a similar API in 2018, which is one of Seb’s theoretical foundations):
component Counter = (props, stateMap) => {
let count = stateMap.get(1, or=0);
let inc = () => stateMap.set(1, count + 1); // functional update
return <a onClick={inc}>{count}</a>
}
However, the mental model that React is closer to and pursuing from implementation to API design is a relatively new pure FP concept - Algebraic Effect. Although the name sounds quite confusing, it actually describes side effects. It is less fancy (less ceramic) and easier to understand than Monad. Dan has a very easy-to-understand blog post for JSers and has a Chinese translation. We can first think of it as a “try-catch that can be resumed”.
To help you understand, what if we had another hypothetical React language……
// hypothetical React lang Ⅱ
component Counter = (props) => {
let count = perform getState(1),or=0); // 1. `perform` "throw" effects to the context
// 4. resume with the continuation to here
let inc = () => perform putState(1, s=count + 1);
return <a onClick={inc}>{count}</a>
}
// call site
try <Counter /> handle // 2.try-handle pattern match effects
// 3. get state from the context and then resume
| getState(key, or) => resume with context.state(key) ?? or
| putState(key, s) => context.state(key)=s; resume with void
We “throw” out of the component to change the state in the “execution context” and then “revert” back……
Even though React has made a lot of efforts to lower the barrier to entry for APIs, its increasingly purely functional thinking does seem to be very “deviant” to more programmers.
So why do we need “pure functional programming”? In addition to the declarative formula, clear data flow, local reasoning, and easy combinability, the academic theoretical support behind it makes it have a very high theoretical ceiling and upside in terms of static analysis and optimization at compile time, high concurrency and high parallelism friendliness at runtime (in recent years, the theoretical research on programming principles has been completely dominated by functional programming)
The core of React’s runtime is cooperative multitasking, which adds low-level capabilities such as concurrency and schedule to React. Many students have only heard of high concurrency in the backend, but in fact, the “ultimate UI” such as a multitasking operating system is a scenario with high concurrency and relies on process scheduling such as time-slicing and re-priortizing. React wants to bring these technologies to UI developers (e.g., Suspense, e.g., Selective Hydration, e.g., Fabrics in RN’s new architecture), the first step is to rewrite the runtime with the Fiber architecture that does not rely on the native call stack, and the second step is to use Hooks to solve the problem that the Class API is not binding enough in purity. Impure components are prone to data race issues in React concurrency mode.
To sum up, the mental model of React in terms of components is “pure functions whose side effects are managed by the context”. As long as you think along this line of thinking, it will be easier to understand “why React tends to use immutable data structures” and “why useEffect defaults to Cleanup will be executed to maintain idempotence”, “Why does React need a cross-rendering ref cell mechanism like useRef to make mutable ref”, “Why does React’s performance optimization include FP-style memoization such as useMemo and Memo”, “Why does React need useMemo useCallback to maintain referential identity”, “Why does React need to use a dependency list for cache invalidation” and other questions.
Supplement
At the end of 2016, I felt that the ultimate difference between React and Vue was “mutable” or “immutable”.
Seb’s brain dump, which received some skepticism after the release of Hooks, wrote:
It’s interesting because there are really two approaches evolving. There’s a mutable + change tracking approach and there’s an immutability + referential equality testing approach. It’s difficult to mix and match them when you build new features on top. So that’s why React has been pushing a bit harder on immutability lately to be able to build on top of it. Both have various tradeoffs but others are doing good research in other areas, so we’ve decided to focus on this direction and see where it leads us.
Not all of them have been translated, but they are the two main divergences in the entire “big frontend” community:
- Variable + change tracking. Including Vue, Angular,
- Immutable + referential equality. Includes React, Elm, (Flutter?)
This divergence is actually a counterpoint to my previous emphasis on “semantics chosen for components”:
- The former is an enhancement to the traditional idea of Imperative Programming (including OOP) with the addition of Reactivity.
- The latter is the extension of traditional Functional Programming in the field of UI development (Functional Reactive Programming?) , but React is implemented in a way that is more “beyond the JS language”.
These two paths are very different from the bottom, which is why React and Vue are drifting apart in everyone’s eyes.
However, I have recently read more about Svelte, SwiftUI and Jetpack Compose, and I have begun to feel that different approaches lead to the same goal. Props are always temporarily input whether they are destroyed following the View or function parameters, and States are always entered temporarily whether they follow the component instance or are set. The foreign exchange must be persistent. As for how to judge updates, mutable state scenarios like array.push are always difficult to track, so we can only show our talents: React wants to pass reference equality automatically, Vue 3 wants to pass Proxy is automatic, but in fact, as long as the change set can be produced, doesn’t Vue2/Svelte/SwiftUI/Compose also work well if the user can manually provide prompts? As long as the changeset can be calculated and passed to the view layer, the view layer will just update (rerender/rebuild/recomposite).
Supplement 2
If you’re relying heavily on Flux (Vuex/Redux, whatever), Vue/React might be more of a dumb/passive layer that’s just for rendering, and the Vue/React differences mentioned above won’t be noticeable, as most of the state management has been thrown to the outer layer.
However, this difference will only become more apparent when considering scenarios where component cohesion is required (i.e. the component itself has its own private state and needs to be self-conatined) and the React Hooks / Vue Composition APIs start to take over more side effects (e.g. IO in addition to state).