React Hooks 深入系列 —— 设计模式

image

React 的 logo 是一个原子图案, 原子组成了物质的表现。类似的, React 就像原子般构成了页面的表现; 而 Hooks 就如夸克, 其更接近 React 本质的样子, 但是直到 4 年后的今天才被真正设计出来。 —— Dan in React Conf(2018)

why Hooks?

一: 多个组件间逻辑复用: 在 Class 中使用 React 不能将带有 state 的逻辑给单独抽离成 function, 其只能通过嵌套组件的方式来解决多个组件间逻辑复用的问题, 基于嵌套组件的思想存在 HOCrender props 两种设计模式。但是这两种设计模式是否存在缺陷呢?

  • 嵌套地狱, 当嵌套层级过多后, 数据源的追溯会变得十分困难, 导致定位 bug 不容易; (hoc、render props)
  • 性能, 需要额外的组件实例存在额外的开销; (hoc、render props)
  • 命名重复性, 在一个组件中同时使用多个 hoc, 不排除这些 hoc 里的方法存在命名冲突的问题; (hoc)

二: 单个组件中的逻辑复用: Class 中的生命周期 componentDidMountcomponentDidUpdate 甚至 componentWillUnMount 中的大多数逻辑基本是类似的, 必须拆散在不同生命周期中维护相同的逻辑对使用者是不友好的, 这样也造成了组件的代码量增加。

三: Class 的其它一些问题: 在 React 使用 Class 需要书写大量样板, 用户通常会对 Class 中 Constructor 的 bind 以及 this 的使用感到困惑; 当结合 class 与 TypeScript 一起使用时, 需要对 defaultValue 做额外声明处理; 此外 React Team 表示 Class 在机器编译优化方面也不是很理想。

useState 返回的值为什么是数组而非对象?

原因是数组的解构比对象更加方便, 可以观察以下两种数据结构解构的差异。

返回数组时, 可以直接解构成任意名字。

[name, setName] = useState('路飞')
[age, setAge] = useState(12)

返回对象时, 却需要多一层的命名。

{value: name, setValue: setName} = useState('路飞')
{value: name, setValue: setName} = useState(12)

Hooks 传递的设计

Hooks 是否可以设计成在组件中通过函数传参来使用? 比如进行如下调用?

const SomeContext = require('./SomeContext)

function Example({ someProp }, hooks) {
  const contextValue = hooks.useContext(SomeContext)
  return <div>{someProp}{contextValue}</div>
}

使用传递的劣势是会出现冗余的传递。(可以联想 context 解决了什么)

Hooks 与 Class 中调用 setState 有不同的表现差异么?

Hooks 中的 setState 与 Class 中最大区别在于 Hooks 不会对多次 setState 进行合并操作。如果要执行合并操作, 可执行如下操作:

setState(prevState => {
  return { ...prevState, ...updateValues }
})

此外可以对 class 与 Hooks 之间 setState 是异步还是同步的表现进行对比, 可以先对以下 4 种情形 render 输出的个数进行观察分析:

是否能使用 React Hooks 替代 Redux

在 React 16.8 版本之后, 针对不是特别复杂的业务场景, 可以使用 React 提供的 useContextuseReducer 实现自定义简化版的 redux, 可见 todoList 中的运用。核心代码如下:

import React, { createContext, useContext, useReducer } from "react"

// 创建 StoreContext
const StoreContext = createContext()

// 构建 Provider 容器层
export const StoreProvider = ({reducer, initialState, children}) => {
  return (
    <StoreContext.Provider value={useReducer(reducer, initialState)}>
      {children}
    </StoreContext.Provider>
  )
}

// 在子组件中调用 useStoreContext, 从而取得 Provider 中的 value
export const useStoreContext = () => useContext(StoreContext)

但是针对特别复杂的场景目前不建议使用此模式, 因为 context 的机制会有性能问题。具体原因可见 react-redux v7 回退到订阅的原因

Hooks 中如何获取先前的 props 以及 state

React 官方在未来很可能会提供一个 usePrevious 的 hooks 来获取之前的 props 以及 state。

usePrevious 的核心思想是用 ref 来存储先前的值。

function usePrevous(value) {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}

Hooks 中如何调用实例上的方法

在 Hooks 中使用 useRef() 等价于在 Class 中使用 this.something。

/* in a function */
const X = useRef()
X.current // can read or write

/* in a Class */
this.X    // can read or write

Is there something like instance variables

Hooks 中 getDerivedStateFromProps 的替代方案

React 暗器百解 中提到了 getDerivedStateFromProps 是一种反模式, 但是极少数情况还是用得到该钩子, Hooks 没有该 api, 那其如何达到 getDerivedStateFromProps 的效果呢?

function ScrollView({row}) {
  const [isScrollingDown, setISScrollingDown] = setState(false)
  const [prevRow, setPrevRow] = setState(null)

  // 核心是创建一个 prevRow state 与父组件传进来的 row 进行比较
  if (row !== prevRow) {
    setISScrollingDown(prevRow !== null && row > prevRow)
    setPrevRow(row)
  }

  return `Scrolling down ${isScrollingDown}`
}

Hooks 中 forceUpdate 的替代方案

可以使用 useReducer 来 hack forceUpdate, 但是尽量避免 forceUpdate 的使用。

const [ignored, forceUpdate] = useReduce(x => x + 1, 0)

function handleClick() {
  forceUpdate()
}

Hooks 中 shouldComponentUpdate 的替代方案

在 Hooks 中可以使用 useMemo 来作为 shouldComponentUpdate 的替代方案, 但 useMemo 只对 props 进行浅比较。

React.useMemo((props) => {
  // your component
})

useMemo 与 useCallback 的区别

useMemo(() => <component />) 等价于 useCallback(<component />)
  • useCallback: 一般用于缓存函数
  • useMemo: 一般用于缓存组件

依赖列表中移除函数是否是安全的?

通常来说依赖列表中移除函数是不安全的。观察如下 demo

const { useState, useEffect } = React

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp) // 这里只输出 1, 点击按钮的 2 并没有输出。
  }

  useEffect(
    () => {
      doSomething()
    },
    [] // 🔴 这是不安全的, 因为在 doSomething 函数中使用了 someProps 属性
  )

  return <div>example</div>
}

export default function() {
  const [value, setValue] = useState(1)
  return (
    <>
      <Example someProp={value} />
      <Button onClick={() => setValue(2)}>button</Button>
    </>
  )
}

在该 demo 中, 点击 button 按钮, 并没有打印出 2。解决上述问题有两种方法。

方法一: 将函数放入 useEffect 中, 同时将相关属性放入依赖项中。因为在依赖中改变的相关属性一目了然, 所以这也是首推的做法。

function Example({ someProp }) {
  useEffect(
    () => {
      function doSomething() {
        console.log(someProp)
      }
      doSomething()
    },
    [someProps] // 相关属性改变一目了然
  )

  return <div>example</div>
}

方法二: 把函数加入依赖列表中

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp)
  }

  useEffect(
    () => {
      doSomething()
    },
    [doSomething]
  )

  return <div>example</div>
}

方案二基本上不会单独使用, 它一般结合 useCallback 一起使用来处理某些函数计算量较大的函数。

function Example({ someProp }) {
  const doSomething = useCallback(() => {
    console.log(someProp)
  }, [someProp])

  useEffect(
    doSomething(),
    [doSomething]
  )

  return <div>example</div>
}

如何避免重复创建昂贵的对象

  • 方法一: 使用 useState 的懒初始化, 用法如下
const [value, setValue] = useState(() => createExpensiveObj)

lazy-initial-state;

  • 方法二: 使用自定义 useRef 函数
function Image(props) {
  const ref = useRef(null)

  function getExpensiveObj() {
    if (ref.current === null) {
      ref.current = ExpensiveObj
    }

    return ref.current
  }

  // if need ExpensiveObj, call getExpensiveObj()
}

相关资料

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