好久没写文章了,开门见山地说,这篇文章介绍几个项目中遇到的关于Hook的问题和解决方法,以及一些用法。
关于Hook是什么欢迎翻阅我的前几篇文章。
之前阅读了一些知乎上关于Hook的文章,有几篇讲心智模型的说得比较好,但没看到太多讲实践的,写一篇抛砖引玉。
1. Everything is Hook
与Everything in Component类似,我在项目里贯彻了Everything is Hook的想法,除了一些逻辑上和道理上不应该使用Hook的代码,大部分的Store Action,Request,多页面连续操作(我们称为Process),可复用组件,通用逻辑都被封装进了一个个Hook里面。
对于复杂的页面我们的做法是将可拆分的逻辑都拆进小的Hook里面,在这个页面的代码里就只剩下了一条主线将这些Hook连接起来。我们有一个页面要应对十几种情况,之前赶工期的写法是copy了十几个文件,每个写自己的逻辑,只做了基本的逻辑拆分。重构以后这个页面变成了这样:
const Page = ({id: string}) => {
const A = useA();
const B = useB();
//大概十几个useXXX
//一个处理数据的函数
somethingManager.getSomethingById(id).composeData({/*anything*/})
return //anything
}
阅读感受大幅提升
2. Smart Component
在以上的想法下,就产生了第二个问题。
Component应不应该是Smart的?虽然在React15的年代大部分人包括我的想法都是只有顶层或者说Page层面的Component这种也叫Container Component,只负责提供实现功能的能力,不负责页面渲染
才能拥有连接Store的权力,但Hook出现以后其带来的便捷性促使我重新审视了自己的想法。
后来我上网查了一些别人的看法,发现Dan Abramov也是这个想法,不愧是偶像
Update from 2019: I wrote this article a long time ago and my views have since evolved. In particular, I don’t suggest splitting your components like this anymore. If you find it natural in your codebase, this pattern can be handy. But I’ve seen it enforced without any necessity and with almost dogmatic fervor far too many times. The main reason I found it useful was because it let me separate complex stateful logic from other aspects of the component. Hooks let me do the same thing without an arbitrary division. This text is left intact for historical reasons but don’t take it too seriously.
把Hook作为一种能力的最小单位的话,很明显Component就应该是Smart的,我们并不希望在使用某种能力的时候,还需要写两段代码把他们combine起来。
但同时我们也有只使用View的需求,咋办呢?把Component拆成两层,一层是View,一层是带有逻辑的Smart Component,同时export出去。
3. Use Redux
Redux也提供了一些Hook形式的API,比如我们项目中使用很多的useSelector
和useDispatch
。具体使用方法不再赘述。
大部分使用形式是这样的:
const Component = () => {
const user = useSelector(state => state.user)
}
或者这样的:
const Component = () => {
const {user} = useSelector(state => ({user: state.user}))
}
有些同学可能会问这两种写法有什么区别啊?
那么就先回答一道经典面试题:
const a = {a: 1, b: 2};
const b = {a: 1, b: 2};
请问 a === b 的值是?
第二种写法可能会造成比较严重的性能问题,因为返回的是一个新的对象,每当这个selecor subscribe到store的变化的时候都会返回一个新的对象,造成React的rerender
这个问题欢迎翻阅我之前关于Hook实现的文章, 简单说一下就是useSelector以后出来的变量会被记在当前的fiber node上,即使这个object里面的属性没变,甚至属性的引用还是之前的引用,但这个object已经是一个新的引用了,所以会触发react的rerender。
解决办法是,要么不在selector里面写任何逻辑,要么想办法给selector加一层cache,我们选用了第二种,引入了reselect。
4. 如何书写一个可以当作Hook使用的Component
这个问题看上去很简单
const useComponent = () => {
//blablablabla
return () => <View>/*blablabla*/</View>
}
const Parent = () => {
const Component = useComponent();
return <View><Component /></View>
}
这样就实现了一个很简单的Component Hook,调的时候直接useComponent
,然后在return里面render一下,完美。
如果要添加一些Component的内部逻辑,我们可以在useComponent里面插入另外的Hooks, 返回的是个Component,当然可以接受props以及拥有自己的内部状态:
const useComponent = () => {
//blablablabla
const data = useData();
const performRequest = usePerformRequest();
return ({title}) => {
const [input setInput] = useState('');
return (
<View>
<Text>{title}</Text>
<Input value={input} onChange={setInput} />
<Map data={data} />
<Button onPress={performRequest} />
</View>)
}
}
这样就实现了一个有逻辑的Component
正当我以为领悟了Hook的真谛时,我们的测试给我报了一个Bug,有个页面输入字母的时候页面会闪烁。
我苦思冥想了半天,终于一拍脑袋,哎?
看一下这个useComponent
的实现,其实是一个高阶函数: () => () => JSX.Element
.
所以当你在Parent里面使用useComponent
的时候,拿到的是一个新的Component,也就是说,如果Parent内部的State变化了,或者useComponent这个高阶函数第一层里面的State变化的时候(Runtime时实际上这里还是Parent的内部state,因为真正意义上的子组件是返回的那个东西)
,正常Component的生命周期是这样的:
mount -> update -> update -> update -> unmount
而这个useComponent
得到的Component们的生命周期是这样的:
Component A: mount -> unmount
Component B: mount -> unmount
Component C: mount -> unmount
如果是个稍微复杂一点的组件,就会导致每次页面render都看起来“清空”
了这个组件的内部状态。
我们给出的解法是useComponent还是和普通的Component一样的写法:
const useComponent = () => {
//blablablabla
return <View>/*blablabla*/</View>
}
但是Parent使用的时候
const Parent = () => {
const component = useComponent();
return <View>{component}</View>
}
看到这里,也许有人要喷了,你这兜兜转转,把use去掉不就是写了一个Component吗?凭什么叫Hook?
让我们发挥一下想象力,useComponent实际是一个Component,在React语境下现在它的l类型是这样的useComponent: FunctionComponent
, 同时它也是一个函数当然也可以是这样: : useComponent = () => JSX.Element
有没有一点灵感?
当我们把它当成一个函数来看待的时候,它的返回并不需要是JSX.Element
我们可以把这个函数改造成这样:
const useComponent = () => {
return {
component: <View>/*anything*/</View>
}
}
const Parent = () => {
const {component} = useComponent();
return <View>{component}</View>
}
很明显,现在它看起来更像一个Hook了,你要问有什么用,请接着看:
const useComponent = () => {
const [num, setNum] = useState(0)
return {
num,
component: <View>
<Text>{num}</Text>
<Button onPress={() => {setNum(num=>num+1}} />
</View>
}
}
让我们抛开Component的固有印象,把它当成一个Hook看,现在返回了两个属性,num
是这个Component内部的状态,component
是它render出的View
Parent现在可以很轻松地知道子Component的内部状态,并作出反应。
子组件甚至可以把操作内部状态的方法export出去,供外面的方法使用。
最后再改造一下这个Hook,让它返回一个Tuple,看起来更像一个原生的Hook:
const useComponent = () => {
const [num, setNum] = useState(0)
return [
num,
<View>
<Text>{num}</Text>
<Button onPress={() => {setNum(num=>num+1}} />
</View>
]
}
const Parent = () => {
const [num, component] = useComponent();
return <View>{component}</View>
}
刚刚想到一个问题,还没有验证:
上面说了useSelector返回一个新对象的话,会造成页面rerender,那么这种Hook的使用方式会导致页面rerender吗?我的结论目前是从原理上分析不会,因为返回的Tuple或者Object并不是使用useState声明的,而useSelector本身使用了React的useContext,其返回的变量会被React认为是当前fiber node的state,而我们返回的Tuple或者Object仅仅是一个内部变量。
现在返回1和2,在4这种写法下,函数的灵活性尽显无余,Component能够export自己内部的变量,很颠覆Class时代自顶而下的Function传递方式,我们自然没必要关心Container Component了。
全文完,感谢阅读,欢迎讨论。
网友评论