7.React 新特性 Hooks 讲解及实例(四)

作者: xiaozhi 发布时间: 2019-09-05 浏览: 2243 次 编辑

使用 Ref Hooks

类组件中使用 Ref 一般有:

  • String Ref

  • Callback Ref

  • CreateRef

上述在函数组件中没有办法使用它们,取而代之的是 useRef Hooks。

useRef 主要有两个使用场景:

  • 获取子组件或者 DOM 节点的句柄

  • 渲染周期之间的共享数据的存储

大家可能会想到 state 也可跨越渲染周期保存,但是 state 的赋值会触发重渲染,但是 ref 不会,从这点看 ref 更像是类属性中的普通成员。

粟例说明一下:获取子组件或者 DOM 节点的句柄

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

粟例说明一下:渲染周期之间的共享数据的存储

function App (props) {
  const [count, setCount] = useState(0);
  let it
  useEffect(() => {
    it = setInterval(() => {
      setCount(count => count + 1)
    }, 1000)
  } , [])

  useEffect(() => {
    if (count >= 5) {
      clearInterval(it)
    }
  })

  return (
    <div style={{padding:'100px'}}>
      <h1>{count}</h1>
    </div>
  )
}

上述使用 useEffect 声明两个副作用,第一个每隔一秒对 count 加 1,因为只需执行一次,所以每二个参为空数组。第二个 useEffect 判断 count 大于等于时,停止对 count 的操作。

运行结果:

显示当 count 为 5 的时候并没有停止,这是为什么呢?

因为在 clearIntervalit 这个变量已经不是 setInterval 赋值时的那个了,每次 App 重渲染都会重置它。这时候就可以使用 useRef 来解决这个问题。

function App (props) {
  const [count, setCount] = useState(0);
  const it = useRef(null)
  
  useEffect(() => {
    it.current = setInterval(() => {
      setCount(count => count + 1)
    }, 1000)
  } , [])

  useEffect(() => {
    if (count >= 5) {
      clearInterval(it.current)
    }
  })

  return (
    ...
  )
}

使用 useRef 来创建一个 it, 当 setInterval 返回的结果赋值给 it 的 current 属性。

运行结果:

你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。

然而,useRef() 比 ref 属性更有用**。它可以很方便地保存任何可变值**,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

自定义 Hook

前面三篇,我们讲到优化类组件的三大问题:

  • 方便复用状态逻辑

  • 副作用的关注点分离

  • 函数组件无 this 问题

对于组件的复用状态没怎么说明,现在使用自定义 Hook 来说明一下。

首先我们把上面的例子用到 count 的逻辑的用自定义 Hook 封装起来:

function useCount(defaultCount) {
  const [count, setCount] = useState(defaultCount);
  const it = useRef()
      
  useEffect(() => {
    it.current = setInterval(() => {
      setCount(count => count + 1)
    }, 1000)
  } , [])

  useEffect(() => {
    if (count >= 5) {
      clearInterval(it.current)
    }
  })
  
  return [count, setCount]
}


function App (props) {
  const [count, setCount] = useCount(0);

  return (
    <div style={{padding: '100px'}}>
      <h1>{count}</h1> 
    </div>
  )
}

运行效果:

可以看出运行效果跟上面是一样的。

定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。我们在函数自定义写法上似乎和编写函数组件没有区别,确实自定义组件与函数组件的最大区别就是输入与输出的区别。

再来一个特别的 Hook 加深一下映像。在上述代码不变的条件下,我们在加一个自定义 Hook 内容如下:

function useCounter(count) {
  return (
    <h1>{count}</h1>
  )
}

在 App 组件调用:

function App (props) {
  const [count, setCount] = useCount(0);
  const Counter = useCounter(count)
  return (
    <div style={{padding: '100px'}}>
      {Counter}
    </div>
  )
}

运行效果:

我们自定义 useCounter Hook返回的是一个 JSX,运行效果是一样的,所以 Hook 是可以返回 JSX 来参与渲染的,更说明 Hook 与函数组件的相似性。

使用 Hook 的法则

只在最顶层使用 Hook

不要在循环,条件或嵌套中调用 Hook,确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这上 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

只在 React 函数中调用 Hook

不要在普通的 JavaScript 函数中调用 Hook, 你可以:

  • 在 React 的函数组件中调用 Hook

  • 在自定义 Hook 中调用其它 Hook

Hooks 常见问题

以下主要说明几个典型的问题,当然这在官网上都有说明。

生命周期方法要如何对应到 Hook?

  • constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState

  • getDerivedStateFromProps:改为 在渲染时 安排一次更新

  • shouldComponentUpdate:详见[官网][9].

  • render:这是函数组件体本身。

  • componentDidMountcomponentDidUpdatecomponentWillUnmount:useEffect Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。

  • componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。

如何强制更新一个 Hooks 组件

如果前后两次的值相同,useState 和 useReducer Hook 都会放弃更新。原地修改 state 并调用 setState 不会引起重新渲染。

通常,你不应该在 React 中修改本地 state。然而,作为一条出路,你可以用一个增长的计数器来在 state 没变的时候依然强制一次重新渲染:

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

  function handleClick() {
    forceUpdate();
  }

可能的话尽量避免这种模式。