從業(yè)務(wù)講React Hook
原文地址:zhuanlan.zhihu.com/p/337846156
作者:張立理
背景
在業(yè)務(wù)中,會有一個挺常見的場景,就是要有一個按鈕,點擊以后能把一段文本復(fù)制到剪貼版里,大量出現(xiàn)在URL、Token、電話號碼之類的地方。
在我們的交互設(shè)計中,一個復(fù)制按鈕可以表現(xiàn)成不同的形式,比如一段文本、一個圖標(biāo)等,當(dāng)它被點擊時,會提示用戶已經(jīng)完成了復(fù)制,并且這個提示會在一段時間后消失:

終版
先來看一下我們怎么快速地實現(xiàn)一個這樣的功能。我們使用了react-copy-to-clipboard來提供復(fù)制的基本功能,并使用了@huse/transition-state來管理狀態(tài)。
import React, {FC, useCallback, ReactElement} from 'react';
import {Tooltip} from 'antd';
import CopyToClipboard from 'react-copy-to-clipboard';
import {useTransitionState} from '@huse/transition-state';
interface Props {
text: string;
children: ReactElement;
}
const CopyButton: FC<Props> = ({text, children}) => {
const [noticing, setNoticing] = useTransitionState(false, 2500);
const copy = useCallback(() => setNoticing(true), [setNoticing]);
return (
<Tooltip visible={noticing} title="已復(fù)制至剪貼板">
<CopyToClipboard text={text} onCopy={copy}>
{children}
</CopyToClipboard>
</Tooltip>
);
};
export default CopyButton;
整體的代碼是比較簡潔的,可以在以下沙盒中試用:
https://codesandbox.io/s/copy-button-o541i?file=/src/CopyButton.tsx:0-703codesandbox.iocopy-button - CodeSandboxCodeSandbox - Copy Buttoncodesandbox.io
分解
作為一個簡單的組件,它的邏輯并沒有什么突出的復(fù)雜度,其中比較關(guān)鍵的是如何讓出現(xiàn)的“復(fù)制成功”的提示信息可以在一段時間后自動消失。
正常情況下,我們會選擇使用一個狀態(tài)來控制提示是否出現(xiàn):
const [visible, setVisible] = useState(false);
const show = useCallback(
() => setVisible(true),
[]
);
而如果我們需要讓它在一定時間后自動消失的話,就勢必要在值改變的時候,打開一個定時器,設(shè)定指定的時間后將值撤銷。我們也知道,凡是遇到定時器的場合,我們就要處理好多次打開定時器之間的競爭關(guān)系。
對于這樣的場景,有2種解法,第一種是在值變更的時候,命令式地打開定時器。但這時你就需要管理好定時器的標(biāo)記,記得把前一次的定時給關(guān)掉:
const timer = useRef(-1);
const show = useCallback(
() => {
clearTimeout(timer.current);
setVisible(true);
timer.current = setTimeout(
() => setVisible(false),
delay
);
},
[delay]
);
切記一點,定時器標(biāo)記這樣的值,它在組件的渲染過程中是不需要的,所以不需要使用一個state去管理,用useRef能保持住值就行。
上面的代碼其實有一些瑕疵,當(dāng)組件銷毀后,定時器依然可能執(zhí)行,調(diào)用一次setVisible,此時在開發(fā)模式下會產(chǎn)生被控制臺里的一個警告,但不會有什么負(fù)面效果。
而另一個辦法,是使用useEffect來觀察值的變化并管理定時器:
useEffect(
() => {
if (visible) {
const tick = setTimeout(
() => setVisible(false),
delay
);
return () => clearTimeout(tick);
}
},
[delay, visible]
);
useEffect帶來的“副作用 - 取消副作用”的方式,可以很方便地管理定時器,也不會產(chǎn)生組件銷毀后定時器仍然執(zhí)行的情況,從復(fù)雜度上來說,我們更愿意選擇這樣的方案。
當(dāng)然上面的代碼依然存在一些瑕疵,當(dāng)delay(也許是從props中來的)變化時,定時器會被取消并生成一次新的定時,但這往往并不是我們想要的效果,因為功能面向用戶,用戶只需要在點擊按鈕出現(xiàn)提示后,提示按照預(yù)期的時間自動消失。
那如果我們不把delay作為useEffect的一個依賴傳遞呢?雖然在行為是完全符合預(yù)期,卻會讓eslint報一個錯,非常不適合強迫癥,也可能導(dǎo)致delay真正發(fā)生變化后,用戶點擊出現(xiàn)的消息并不按最后的delay時間消失。
所以在這里,我們就要啟用useRef的“作弊模式”。eslint的規(guī)則會判斷一個值是否為ref,并識別其不需要加入到useEffect、useCallback等的依賴中。當(dāng)一個值并不會影響渲染,也不需要引發(fā)副作用時,使用useRef去托管就是一個很好的選擇。
const delayRef = useRef(delay);
useEffect(() => {
delayRef.current = delay;
}, [delay]);
useEffect(() => {
if (visible) {
const tick = setTimeout(() => {
setVisible(false);
}, delayRef.current);
return () => clearTimeout(tick);
}
}, [visible]);
而把這些邏輯串起來,形成“一個變化后會自動變回去的狀態(tài)”這樣的概念,額外再抽象一些能力,比如:
可以是什么類型,不局限于boolean,并可以指定初始值。 可以設(shè)定默認(rèn)的持續(xù)時間。 可以在每一次修改狀態(tài)時,指定一個臨時的持續(xù)時間。 允許在持續(xù)過程中手動設(shè)置回默認(rèn)值。
總結(jié)
從一個簡單的復(fù)制按鈕的交互開始,在這一篇中重點講解了如何使用狀態(tài)+定時器的組合來實現(xiàn)一個過渡式的狀態(tài),并讓狀態(tài)自動返回初始值,其中的要點有:
與渲染無關(guān)的數(shù)據(jù)可以使用useRef存儲,不需要useState管理狀態(tài)。 可以使用命令式或useEffect的方式管理定時器,但往往使用useEffect更為方便,也能照顧到組件銷毀時的情況。 對于不希望引發(fā)useEffect的數(shù)據(jù),可以使用useRef管理形成一種“作弊”騙過eslint,同時確保能在useEffect的閉包中取到最新的值。
這個hook可用在所有臨時出現(xiàn)的場景,包括提示信息、消息氣泡等,一定程度上配合CSS的動畫能取得更好的效果。
1.看到這里了就點個在看支持下吧,你的「點贊,在看」是我創(chuàng)作的動力。
2.關(guān)注公眾號
程序員成長指北,回復(fù)「1」加入高級前端交流群!「在這里有好多 前端 開發(fā)者,會討論 前端 Node 知識,互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長。
“在看轉(zhuǎn)發(fā)”是最大的支持
