從業(yè)務(wù)講React Hook

關(guān)注公眾號(hào)?前端人,回復(fù)“加群”
添加無(wú)廣告優(yōu)質(zhì)學(xué)習(xí)群
原文地址:zhuanlan.zhihu.com/p/337846156
背景
在業(yè)務(wù)中,會(huì)有一個(gè)挺常見的場(chǎng)景,就是要有一個(gè)按鈕,點(diǎn)擊以后能把一段文本復(fù)制到剪貼版里,大量出現(xiàn)在URL、Token、電話號(hào)碼之類的地方。
在我們的交互設(shè)計(jì)中,一個(gè)復(fù)制按鈕可以表現(xiàn)成不同的形式,比如一段文本、一個(gè)圖標(biāo)等,當(dāng)它被點(diǎn)擊時(shí),會(huì)提示用戶已經(jīng)完成了復(fù)制,并且這個(gè)提示會(huì)在一段時(shí)間后消失:

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