使用 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
的时候并没有停止,这是为什么呢?
因为在 clearInterval
, it
这个变量已经不是 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
:这是函数组件体本身。componentDidMount
,componentDidUpdate
,componentWillUnmount
:useEffect Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。componentDidCatch
andgetDerivedStateFromError
:目前还没有这些方法的 Hook 等价写法,但很快会加上。
如何强制更新一个 Hooks 组件
如果前后两次的值相同,useState 和 useReducer Hook 都会放弃更新。原地修改 state 并调用 setState 不会引起重新渲染。
通常,你不应该在 React 中修改本地 state。然而,作为一条出路,你可以用一个增长的计数器来在 state 没变的时候依然强制一次重新渲染:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0); function handleClick() { forceUpdate(); }
可能的话尽量避免这种模式。