关于Hook的二三事

好久没写文章了,开门见山地说,这篇文章介绍几个项目中遇到的关于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,比如我们项目中使用很多的useSelectoruseDispatch。具体使用方法不再赘述。
大部分使用形式是这样的:

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了。

全文完,感谢阅读,欢迎讨论。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268