Obviously?
Someone on Zhihu (a Chinese Q&A platform similar to Quora) asked:
React Hooks feel counter-intuitive. The component function re-runs on every render,
useStaterelies on a global linked list under the hood,useEffect's semantics aren't straightforward, and you needuseCallbackjust to persist a function. Vue 3's Composition API borrowed some ideas from Hooks, but its approach feels more natural:setupruns once, and you return a render function, using plain JS closures to persist component state across renders. Can React adopt Vue's approach to improve its own Hooks?
Well. No. They stand for very different mental models. A lot of people see the surface similarity and miss how differently the two frameworks think underneath. Let me explain.
Frameworks and libraries are abstractions built on a language, but that doesn't mean a framework's design has to stay inside the idioms and paradigms of the language it's written in.
JavaScript has a few very Lisp-like traits: first-class functions, dynamic typing, and some macro support (Babel, say). Because of them, front-end frameworks have been looking more and more like programming language design. A framework now comes with its own syntax (its DSLs and APIs), its own semantics (some kept from JavaScript, some added by the APIs), a runtime that runs the whole thing, and a mental model that all of it expresses.
Vue 3, "Reactive (Closure-based) OOP"
Let's start with Vue (here "Vue" means Vue with the Composition API).
const Counter = {
setup(initialProps) {
const count = reactive({count: 0}) // or `ref(0)`
const inc = () => { count.value++ }
return {count, inc}
}
template: "..."
}Vue models a component as an object. setup runs only once and returns an object that gets merged onto the Counter component. That object and everything on it are persistent references, including the state count captured in the inc closure. Rendering is the component's template field made real.
The main thing Vue adds to the language is reactivity (on mutable data). The state count is a reactive object, inc changes state by modifying count directly, and a change in state runs every watcher, which includes re-rendering and running side effects (watchEffect). All of it is driven by reactivity.
Some people (like the person asking the question) wonder: if setup returns a render function instead and the component's variables live in closures, is it still an object?
return (props) => <a onClick={inc}>{count.value}</a>Yes. For the state to persist, Vue has to keep the same function and the same closure around from render to render. That's the old JavaScript trick of using closures to fake private fields on an object[1]. ("A closure is a poor man's object, and an object is a poor man's closure.")
To make this concrete, imagine there were a 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> }
}Looks a lot like class-based OOP, right? Except Vue's objects are singletons (or closures), not classes (or prototypes), and their members have reactive magic cast on them. That's how Vue boils down to an OOP mental model, at least in concept. Vue as a whole (state management, how data flows) still owes a lot to FP, though, and is quite different from traditional OOP like Java[2][3].
At the heart of Vue's runtime is dependency tracking. It does two things. It keeps reactivity mostly out of the user's way, since dependencies are collected automatically and there's much less to think about. And because the tracking is very fine-grained and Vue's templates are very static, Vue can re-render exactly what changed and nothing else, without being told.
So in Vue, a component is "an object that owns its data and behavior and reacts on its own." Seen this way, a lot of things make sense: why Vue can use mutable data structures, why it needs ref to wrap plain values, and the advantages its RFC claims over React Hooks, like feeling closer to everyday JavaScript (that one's subjective), putting less pressure on GC, and not needing you to declare dependencies by hand.
React, "Purely (Semi-Monadic/Algebraic) FP"
Now React (here "React" means React with Hooks).
function Counter(props) {
const [count, setCount] = React.useState(0);
const inc = () => setCount(count + 1)
return <a onClick={inc}>{count}</a>
}React models a component as a function, and every render really does call Counter. Each call runs useState, which fetches the current state from React into count, and each call creates a new inc whose closure captures the new count.
The main thing React adds is an evaluation context that controls side effects. In plain terms, that's the React runtime. The state count has to be read from the context on every call. inc updates state by asking setCount to change what's in the context. A state change calls the function again, which reads the new state from the new context, re-renders, and schedules side effects that the context controls (useEffect).
Again, imagine there were a 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>
}Looks a lot like Haskell with monads, right? Except React designed its API so you never have to learn any of that[4]. If you don't know monads, a core idea in pure FP, loosely[5] think of one as the "context" above. I'm dropping the M-word because monads are the standard way pure FP handles side effects, and that makes it easier to show how React boils down to pure FP.
Some people (like @题叶) ask: "These don't look like the pure functions I know. Is this really purely functional?" Well, desugar it:
component Counter = (props) =>
context.state(1).get_or(0).then([count, put] => { // `then` is monadic bind.
let inc = () => put(count + 1)
return <a onClick={inc}>{count}</a>
}).unwrap() // assuming it's safe.Remind you of anything? Promises, which are also monads, with an async context?
Simplify it even further, too far really, and you get plain state-passing style. The React team actually considered an API like this in 2018[6], and it's part of the theory Seb built on[7]:
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>
}What React's implementation and API actually sit closer to, and are moving toward[8], is a newer idea from pure FP: algebraic effects. The name is scary, but for describing side effects it takes less ceremony than monads and is easier to follow. Dan wrote a very approachable post about it for JS developers[9] (there's a Chinese version too[10]). For now, think of it as a try-catch you can resume from.
One more time, imagine a 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 voidGetting the feel of it? The component "throws" a state change out to the evaluation context, the context handles it, and then we "resume" where we left off...
React has worked hard to make its API easy to pick up, but its more and more purely functional way of thinking still strikes many programmers as heresy.
So why bother with purely functional programming? You probably know the usual reasons: it's declarative, data flow is clear, you can reason locally, things compose easily. On top of that, the theory behind it leaves a lot of room to grow, both in static analysis and optimization at compile time and in concurrency and parallelism at runtime. (Programming language research these days is almost all functional.)
Right now React's runtime is focused on cooperative multitasking, with concurrency and scheduling built into the foundation. Many people think of concurrency as a back-end problem, but a multitasking OS, the ultimate UI, is a highly concurrent system too, and it schedules processes with tricks like time-slicing and re-prioritizing. React wants to hand those tricks to UI developers (Suspense, Selective Hydration, the new Fabric architecture in React Native). Step one was rewriting the runtime as Fiber, which doesn't rely on the native call stack. Step two was Hooks, because the class API couldn't enforce purity well enough, and impure components in concurrent mode are very prone to data races.
So in React, a component is "a pure function whose side effects are managed by the context." Seen this way, a lot of things make sense: why React prefers immutable data, why useEffect cleans up by default to stay idempotent, why React needs useRef as a ref cell for mutable values that outlive a render, why its performance tools are FP-style memoization like useMemo and memo, why it needs useMemo and useCallback to keep references stable, and why it uses dependency arrays to invalidate caches.
Addendum
Back in late 2016 I already felt the biggest difference between React and Vue (or one of the biggest) was mutable vs. immutable.

After Hooks came out, Seb answered some of the doubts in a brain dump[11]:
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.
In short, the front-end world has split into two main paths:
- Mutable + change tracking. Including Vue, Angular.
- Immutable + referential equality. Including React, Elm, (Flutter?)
That split is the flip side of what I said above about how each framework models a component:
- The first takes traditional imperative programming (OOP included) and adds reactivity.
- The second is traditional functional programming finding its way into UI (functional reactive programming?), except React does it in a way that goes somewhat beyond what JavaScript itself offers.
The two are different all the way down, which is probably why React and Vue look to most people like they're drifting further apart.
Then again, looking at Svelte, SwiftUI[12] and Jetpack Compose[13] lately, I see things coming back together. Props are always short-lived inputs, whether they die with the view or get passed in as function arguments. State always has to persist, whether it lives on a component instance or somewhere outside. Detecting updates is the hard part, since mutations like array.push are always tricky to track, and everyone solves it their own way. React wants to do it automatically with reference equality, Vue 3 with Proxy, but anything that produces the set of changes will do. Vue 2, Svelte, SwiftUI and Compose all have the user give hints by hand, and they work fine. Once you have the set of changes and hand it to the view layer, all the view layer has to do is update (rerender, rebuild, recompose).
Addendum 2
If you lean heavily on Flux (Vuex, Redux, whatever), Vue and React start to look like passive rendering layers, and the differences above matter less, since most of your state has already moved out into another layer.
But components that need to be encapsulated, with private state of their own, aren't going away, and React Hooks and Vue's Composition API are taking on more side effects than state (IO, for one). So the split will only get more obvious.
That's all.
References
- JavaScript 模块化七日谈 - 黄玄的博客 Hux Blog https://huangxuan.me/2015/07/09/js-module-7day/
- 如何理解尤雨溪在 2019 VueConf 上所讲的 UI 类框架很少使用面向对象的特性这件事?- 黄玄的回答 https://www.zhihu.com/question/328958700/answer/714287394
- 前端是否适合使用面向对象的方式编程?- 黄玄的回答 https://www.zhihu.com/question/329005869/answer/739525268
- React Hooks的引入会对之后的React项目开发产生什么影响?- 黄玄的回答 https://www.zhihu.com/question/302916879/answer/536846510
- React composes its contexts at runtime with a linked list kept in call order, not with parameterized layers of polymorphism (monad transformers, say). Note that both are linear.
- State-passing Style Hooks https://mobile.twitter.com/acdlite/status/971598256454098944
- https://github.com/reactjs/react-basic
- You can think of calling a context or a Hook as unwinding the stack and then resuming a continuation. And row polymorphism is linear too.
- Algebraic Effects for the Rest of Us https://overreacted.io/algebraic-effects-for-the-rest-of-us/
- 通俗易懂的代数效应 https://overreacted.io/zh-hans/algebraic-effects-for-the-rest-of-us/
- Why React https://gist.github.com/sebmarkbage/a5ef436427437a98408672108df01919
- https://swiftwithmajid.com/2019/06/12/understanding-property-wrappers-in-swiftui/
- https://developer.android.com/jetpack/compose/state#remember
AI-translated from the original