原文地址:https://medium.com/swlh/the-ugly-side-of-hooks-584f0f8136b6
译者注:文章中没有把Funclass翻译成函数类,是因为原作者发明的这个词真的很贴切.
在这篇文章中,我会分享我关于React Hooks的观点,就像我文章的题目表述的一样,我对React Hooks并不感冒。
我们来分析一下放弃类转而使用Hooks的具体动机,正如React官方文档描述的那样:
动机
#1: 类是令人疑惑的
我们发现类是学习React的一大障碍。你需要了解Javascript中this的工作机制,而Javascript中的this又偏偏和其他大多数的语言不通。你必须要记住要将this绑定到事件处理函数上。如果没有这个不稳定的语法提案/插件(https://babeljs.io/docs/en/babel-plugin-transform-class-properties/),代码将会变得非常冗长。React中函数组件和类组件使用的区别,甚至让经验丰富的React开发者,也会产生理解或使用的分歧。
我同意上面说到的,它确实会让你感觉到困惑,当你刚入门Javascript的时候。但箭头函数已经解决了这个问题。而把一个已经被Typescript支持的stage3特性称为“不稳定的语法提案”的描述,只是纯粹的煽风点火。Reac团队提到的是关于class field 语法,而这是一种已经被广泛使用并可能很快得到官方支持的语法。
class Foo extends React.Component {
onPress = () => {
console.log(this.props.someProp);
}
render() {
return <Button onPress={this.onPress} />
}
}
正如你看到的上面这段代码一样:通过使用支持class field的箭头函数,你不需要在contructor中绑定任何东西,this都会指向正确的上下文。
并且,如果类是令人困惑的,那一个新的hook函数不也是吗?一个hook函数,不是一个常规的函数,因为它包含了state,它看起来很奇怪(比如useRef),并且它可以包含多个实例。它绝不是类,它介于类和函数之间。并且从现在开始我打算把它叫做Funclass(函数类)。所以,这些Funclasses真的对人和机器来说会变得更容易吗?我不清楚机器会不会觉得更容易,但我并不觉得Funclasses从概念上会比类来得更容易理解。类是一个被大家所熟知的一个概念,每个开发人员或多或少都熟悉this的含义,即使在Javascript中有些许的不同。Funclasses则不一样,这是一个全新且奇怪的概念。它让人感觉更像黑科技,它更依赖于约定,而不是严格的语法限制。你必须遵循一些严格而奇怪的规则,你需要了解应该在哪写地方编写你的代码,并且它还包含了不少的陷阱。你还需要为一些可怕的命名做好准备比如:useRef(this的一个更具有想象力的别名),useEffect,useMemo,useImperativeHandle(这个命名它说的是啥??)等等。
类的语法是为了处理多个实例和实例上下文的概念而专门发明的。Funclasses只是为了达到某些目标的奇怪方式。很多人把FunClass和函数式编程混淆了,但是FunClass实际上只是伪装的类。类是一个概念,而不是语法。
官方文档还提到:
React中函数组件和类组件使用的区别,甚至让经验丰富的React开发者,也会产生理解或使用的分歧。
到目前位置,其实区别是非常明显的——那就是你如果需要使用state和生命周期方法,那你就需要class否则你使用函数式组件或者类组件都可以。就我个人而言,我更喜欢这样的方式,当我看到一个函数式组件的时候,我就知道它是一个不需要内部状态的组件。可悲的是,由于Funclass的出现,这将不再适用。
#2: 包含状态的逻辑在组件间难以复用
React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props 和 高阶组件。
这不是很讽刺吗?React最大的问题,在我看来是它没有提供一个开箱即用的状态管理解决方案。并留给我们的是关于如何填补这一空白的长期争论,并为一些非常糟糕的设计模式(如Redux)打开了大门。在经历了多年的沮丧之后,React团队终于做了表态,将可复用性行为“附加”到组件很困难…谁猜得到呢。总之,Hooks能让这种情况变得更好吗?答案是并不一定。Hooks无法和class一起工作,如果你已有的代码都是用class一起编写的,你仍然需要其他的方式来共享带有状态的逻辑。当然,Hooks也只解决了共享单个实例内的逻辑,如果我们想要在多个实例之间共享state,你仍然需要使用stores以及第三方的状态管理解决方案。现在是时候让React采取行动实现一个更适合的状态管理工具来管理全局状态(stores)和本地状态(每个实例),从而彻底消除这个漏洞。
#3: 复杂的组件变得越来越难以理解
我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。[…] 相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。[…] 为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
如果你已经使用了stores,这一论点则几乎毫不相关。我们来看看为什么:
class Foo extends React.Component {
componentDidMount() {
doA();
doB();
doC();
}
}
你可以看到,在例子中我们把不相关的逻辑一起放到了componentDidMount函数之中,但这会让我们的组件变得庞大不可维护吗?当然不是,具体的实现仍然是在class之外,状态仍然保留在store之中。如果没有sotre(全局或局部的状态),这些带有状态的逻辑仍然需要在class组件中实现,那样class中的代码就的确会变得更臃肿。但同时,它更像React在解决不使用状态管理工具下的场景而引发的问题。在实际应用中,大型的应用都已经存在了状态管理工具,这让这些共享状态/逻辑的问题实际上已经得到了缓解。当然在大多数场景中,我们可以把这个组件拆分成更小的组件再组合起来使用。
使用 Funclasses 我们的代码写起来像这样:
function Foo() {
useA();
useB();
useC();
}
它看起来好像更清晰了,但真的是吗?我们需要使用3个不同的useEffect hook,在最后我们会写一些代码,来看看我们在useEffect都做了什么。在类组件中,你第一眼就可以看出组件mount之后要做哪些事情。在Funclass的例子中,你需要查询所有的useEffect,哪些包含一个空的数组依赖,这样你才能找到所有在组件mount之后要做哪些事情。生命周期方法的声明性本质基本上是一件好事,我发现要研究Funclasses的执行流程要困难得多。我看过了很多的例子,Funclasses让开发者更容易写出糟糕的代码,我们后面会看一看相关的例子。
但首先,我必须承认useEffect有不少的优点,请看下面的示例:
useEffect(() => {
subscribeToA();
return () => {
unsubscribeFromA();
};
}, []);
useEffect hook 让我们可以把监听和取消监听的方法成对的整合在一起,这让我们的代码看起来更整齐.就和配对的componentDidMount和componentDidUpdate一样. 在我的以往经验中, 这些例子并不常见,但它确实是很有效的使用案例,在这种场景里useEffect确实能让代码更简洁更易懂。但问题在于为什么我们必须使用Funclass而只是为了能使用useEffect,为什么我们不能在类组件中也有相应的用法,比如:
class Foo extends React.Component {
someEffect = effect((value1, value2) => {
subscribeToA(value1, value2);
return () => {
unsubscribeFromA();
};
})
render(){
this.someEffect(this.props.value1, this.state.value2);
return <Text>Hello world</Text>
}
}
上面的effect函数会记住传入的函数,并且会在依赖它的属性改变之后重新触发。在render里调用我们的someEffect能确保一旦任意一个参数改变了,我们的函数都会重新执行一遍。通过这种方式,我们通过将componentDidMount
和componentDidUpdate
结合,达到了到了一种和useEffect类似的的功能。不过很遗憾我们仍需要在componentWillUnmount
里执行最后一次的取消监听。虽然在render里调用effect函数看起来有些不够优雅。但为了达到和useEffect一样的结果,React需要对此做一定的支持。
因此,useEffect不应该被认为是将类组件转移到Funclass的有效动机之一,在类组件里的也应该支持这种方式。
你可以在这里查询effect函数的具体实现以及在这里查看相关的例子working example.
#4: 性能_
我们发现使用 class 组件会无意中鼓励开发者使用一些让优化措施无效的方案。class 也给目前的工具带来了一些问题。例如,class 不能很好的压缩,并且会使热重载出现不稳定的情况。因此,我们想提供一个使代码更易于优化的 API。
React团队说类组件更难以优化和压缩,而Funclasses可能会在某种方面改变这一点。好吧,关于这点我想说的只有:拿出你的证据
我找不到任何与此相关的报道,或者任何的示例Demo我可以下载下来进行运行验证Funclasses和classes的性能对比。真实情况是,我们并没有足够的Funclasses示例需要进行改进,而我们也将预料到,会导致类组件难以优化的情况,在Funclasses中仍然会出现。
总之,所有关于性能问题的争辩在没有具体数据指标的为依据是没有任何意义的,所以我们无法把它做为支持我们该用hooks的一个论点。
#5: Funclass 更简洁
你会发现很多将类组件转换为Funclass的例子,但大多数例子都是使用了useEffect把componentDidMount和componentWillUnmount 中的代码结合起来来进行优化。就像我前面说的一样,useEffect 不应该仅仅是Funclass的优势,如果您忽略了它所实现的代码缩减,您只会受到很小的影响。如果您尝试使用useMemo、useCallback等来优化函数类,那么您甚至可能得到比原来更冗长的代码。当对比这些小而琐碎的组件时,Funclass确实更出色,因为类组件有一些固有代码格式,不管多小的组件,你都需要把它编写出来。但如果对比比较复杂的大型组件,我想有可能很难比较出两者谁更简洁,而且或许类组件会更干净易读。
最后,我会谈论一些和useContext相关的内容:useContext和原有在类中使用的context API相比确实是一项巨大的变动。但我仍然无法在类组件中使用useContext?为什么我们不能像下面的代码这样使用呢:
//inside "./someContext.jsx" :
export const someContext = React.Context({helloText: 'bla'});
//inside "Foo.jsx":
import {someContext} from './someContext';
class Foo extends React.component {
render() {
<View>
<Text>{someContext.helloText}</Text>
</View>
}
}
当helloText改变时,组件会自动监听值的改变并重新渲染。这样就不需要丑陋的高阶函数了。
所以,为什么React团队选择只改进了useContext API,而不是原有的context API?我也不知道,但它并不意味着Funclass本质上是简洁的。它意味着React应该做额外更多的努力,来让这些改进也能惠及类组件。
====================我是一个快乐的分割线====================
在我们谈论了关于从类组件改进到hooks的动机之后,我再来谈一谈我所不喜欢hooks的部分:
其他缺点
隐含的副作用
其中一个困扰我的是useEffect的实现,大都缺少相关副作用的清晰展示。在类组件里,你想要知道组件mount的时候都执行了哪些代码,你只要查找componentDidMount 和构造函数就可以了。而在使用了useEffect hook之后,这些副作用会被隐藏的更深。
我们来看看下面这个例子:
const renderContacts = (props) => {
const [contacts, loadMoreContacts] = useContacts(props.contactsIds);
return (
<SmartContactList contacts={contacts}/>
)
}
上面的例子没有什么特殊的地方。我们需要调查SmartContactList 或者我们应该深入研究useContacts ?我们先看看useContacts:
export const useContacts = (contactsIds) => {
const {loadedContacts, loadingStatus} = useContactsLoader();
const {isRefreshing, handleSwipe} = useSwipeToReresh(loadingStatus);
// ... many other useX() functions
useEffect(() => {
//** lots of code, all related to some animations that are relevant for loading contacts*//
}, [loadingStatus]);
//..rest of code
}
所以到底哪里隐藏了副作用?如果我们再进入useSwipeToRefresh我们会发现:
export const useSwipeToRefresh = (loadingStatus) => {
// ..lot's of code
// ...
useEffect(() => {
if(loadingStatus === 'refresing') {
refreshContacts(); // bingo! 隐藏起来的副作用!
}
}); //<== we forgot the dependencies array!
}
我们发现了隐藏的副作用。refreshContacts 会在组件渲染时偶尔调用contracts的查询。在大型项目中,一些没有良好组织的,嵌套的useEffect 会导致一些严重的bug。
我并不是说使用了类组件就不会写出这些糟糕的代码,但Funclass会更容易导致错误,并且没有了严格的生命周期函数的约束,它更容易写出一些糟糕的实现。
臃肿的API
通过增加hooks API,React的API数量差不多增加了一倍。所有开发者都需要学习两种方式的编写逻辑。而且我想说,新的API比老得更不容易理解。一些简单的知识点,比如:如何获得上一个props和state,也已经成为一些很好的面试题。但如果不通过google,你能使用hook写出获取上一个props的值的代码吗?
像React如此被大众广泛使用的库应当对增加如此大改动的API格外小心,因此这些动机甚至于有些不合理。
缺少声明
以我的观点,Funclass写出的代码会比类组件更凌乱。比如,要找到组件入口的时候,通常在类组件里,我们只要查找render函数就行,但在Funclass里,会很难找出最主要的return语句。但让要追踪不同的useEffect表达式,以及理解正确的组件运转逻辑也变得很困难。在类组件中,我们则可以通过生命周期函数给予的提示来阅读相应的代码和逻辑流程:需要了解初始化的逻辑,我会切换到componentDidMount 函数;如果要寻找数据更新后的逻辑,我只需要切换到componentDidUpdate 即可。而在Funclasses编写的庞大组件中,我很难发现相应的方法可以帮助我做到上面的事情。
把所有都耦合到React
我们来看一下下面这个跟踪定位的hook,通过一个‘react-use’的包导入:
import {useLocation} from 'react-use';
const Demo = () => {
const state = useLoaction();
return (
<div>
{JSON.stringify(state)}
</div>
)
}
但是不是使用一个纯粹的不依赖React的第三方包会更好?比如:
import {tracker} from 'someVanillaJsTracker';
const Demo = () => {
const [location, setLocation] = useState({});
useEffect() {
tracker.onChange(setLocation);
}, []);
return (
<div>
{JSON.stringify(state)}
</div>
);
};
它会让代码变得更冗长?是的。前一个例子写起来更简短。但第二个解决方案,能保证JS和React生态解耦,为这么重要的事情多添加几行代码花费的代价并不大。自定义hooks为将纯逻辑耦合到React的状态打开了一扇大门,而这些库正在像野火一样蔓延。
它让我感觉不太对
许多年以前,我从Angular 1.5转向了React,我很惊讶React的API是如此的简单,文档也是如此的简洁。与此同时angular的文档则非常多,也需要我去理解更多的概念。React从那时来看,它给我的感觉是对的,它的代码干净且简洁,只需要花上几个小时就能把所有文档阅读完毕。而开始使用hooks之后,我发现我在使用过程中,一而再再而三的回来查询文档,一遍又一遍。
总结
我也不愿意给大家浇冷水,但我真的认为Funclasses可能是React社区发生的第二糟糕的事情(第一名仍然被Redux占据)。它给已经脆弱的生态系统又增加了一个毫无意义的争论,现在我还不清楚Funclass是被推荐的方式,还是仅仅是另一个特性,目前来说这是个人喜好的问题。
我希望React社区能醒悟过来,要保持Funclass和类组件的特性之间的对等性。我们可以在类中有一个更好的上下文API,我们可以对类使用useEffect,甚至useState。如果我们愿意,React应该给我们继续使用类的选择,而不是通过只为functlases添加更多特性而将类组件遗忘并强行扼杀它。
顺便说下,回到2017年,我发布了一篇文章《The ugly side of Redux》,时至今日Redux的创造者也承认Redux是一个错误。
历史是否会重演,时间会证明。
不管怎样,我和我的同事们决定暂时坚持使用类组件,并使用基于Mobx的解决方案作为状态管理工具。我认为hook在solo开发人员和在团队中工作的人的流行程度有很大的不同——hook的坏特性在一个大的代码库中更明显,。就我个人而言,我真的希望React可以撤销所有的hooks。
我将开始研究一个RFC,它将为React提供一个内置的简单、干净的状态管理解决方案,它将彻底解决共享有状态逻辑的问题,希望这是一种比Funclass更不笨拙的方式。