在 React Hooks 中使用 Typescript 小記
最近在關(guān)注 Typescript 和 react hook 相關(guān)的知識(shí)昂验,看到了這篇文章缘滥,還不錯(cuò),get 到了惩坑。感謝作者的分享如绸。
原文:Notes on TypeScript: React Hooks
原文作者簡(jiǎn)介:A. Sharif:專注于質(zhì)量。軟件開發(fā)旭贬。產(chǎn)品管理怔接。
https://twitter.com/sharifsbeat
busypeoples簡(jiǎn)介
這些筆記有助于更好地理解 TypeScript,并且在某些特定情況下如何使用 TypeScript 也會(huì)有幫助稀轨。所有示例都基于TypeScript 3.2扼脐。
React Hooks
在 “Notes on TypeScript"(Typescript 小記) 系列的這一部分中,我們將了解如何在 React Hooks 中使用 TypeScript奋刽,并了解更多關(guān)于React Hooks 的知識(shí)瓦侮。
我們將參考官方 React 文檔關(guān)于 Hook 的文檔,當(dāng)需要了解更多關(guān)于 hook 的信息或需要特定問題的特定答案時(shí)佣谐,這是一個(gè)非常有價(jià)值的資源肚吏。
在一般情況下,在16.8中已經(jīng)添加了 Hook 來進(jìn)行響應(yīng)狭魂,并使開發(fā)人員能夠在函數(shù)組件中使用 State (狀態(tài))罚攀,在此之前,只有在類組件中才可能使用 State (狀態(tài))雌澄。文檔說明有一些基本的 Hook API 和其他的 Hook API斋泄。
基本的 Hooks 有 useState
, useEffect
, useContext
,還有一些其他的镐牺,比如useReducer
, useCallback
, useMemo
, useRef
.
useState
讓我們從 useState 開始炫掐,這是一個(gè)基本的 hook,見名知義睬涧,它應(yīng)該用于狀態(tài)處理募胃。
const [state, setState] = useState(initialState);
查看上面的示例,我們可以看到 useState 返回一個(gè)狀態(tài)值以及一個(gè)更新它的函數(shù)畦浓。但我們?nèi)绾屋斎?state 和 setState 呢?
有趣的是痹束,TypeScript 可以推斷類型,這意味著通過定義initialState宅粥,可以推斷狀態(tài)值和更新函數(shù)的類型参袱。
const [state, setState] = useState(0);
// const state: number
const [state, setState] = useState("one");
// const state: string
const [state, setState] = useState({
id: 1,
name: "Test User"
});
/*
const state: {
id: number;
name: string;
}
*/
const [state, setState] = useState([1, 2, 3, 4]);
// const state: number[]
上面的例子很好地說明,我們不需要進(jìn)行任何手工輸入。但如果沒有初始狀態(tài)呢抹蚀?當(dāng)嘗試更新狀態(tài)時(shí)剿牺,上面的示例會(huì)中斷。
我們可以在需要時(shí)使用 useState 手動(dòng)定義類型环壤。
const [state, setState] = useState<number | null>(null);
// const state: number | null
const [state, setState] = useState<{id: number, name: string} | null>(null);
// const state: {id: number; name: string;} | null
const [state, setState] = useState<number | undefined>(undefined);
// const state: number | null
同樣值得注意的是晒来,與類組件中的 setState 相反,使用 hook 的更新函數(shù)需要返回完整的狀態(tài)郑现。
const [state, setState] = useState({
id: 1,
name: "Test User"
});
/*
const state: {
id: number;
name: string;
}
*/
setState({name: "New Test User Name"}); // Error! Property 'id' is missing
setState(state => {
return {...state, name: "New Test User Name"}
}); // Works!
另一件值得注意的有趣的事情是湃崩,我們可以通過將函數(shù)傳遞給useState 來惰性地設(shè)置狀態(tài)。
const [state, setState] = useState(() => {
props.init + 1;
});
// const state: number
同樣接箫,TypeScript 可以推斷狀態(tài)類型攒读。
這意味著我們?cè)谑褂?useState 時(shí)不需要做太多工作。只有在沒有初始值的情況下才需要加入類型限制辛友,因?yàn)橛谐跏贾禃r(shí)可以推斷出實(shí)際狀態(tài)的類型薄扁。
useEffect
另一個(gè)基本的鉤子是 useEffect,它在處理副作用時(shí)非常有用废累,比如日志記錄邓梅、突變或訂閱事件偵聽器。useEffect 可以傳遞一個(gè)函數(shù)邑滨,運(yùn)行這個(gè)函數(shù)可以執(zhí)行一些清除功能日缨,比如清除訂閱信息或者監(jiān)聽器,這是非常有用的掖看。此外 useEffect 提供了第二個(gè)參數(shù)匣距,包含一組值,確保傳遞給 useEffect 的函數(shù)乙各,只有當(dāng)這個(gè)值改變了墨礁,函數(shù)才會(huì)執(zhí)行。這讓我們可以控制何時(shí)運(yùn)行函數(shù)處理副作用耳峦。
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source]
);
以文檔中的原始示例為例,我們可以注意到在使用 useEffect 時(shí)不需要任何額外的類型焕毫。
當(dāng)我們?cè)噲D返回不是函數(shù)或 effect 函數(shù)中未定義的內(nèi)容時(shí)蹲坷,TypeScript 會(huì)發(fā)出報(bào)錯(cuò)信息。
useEffect(
() => {
subscribe();
return null; // Error! Type 'null' is not assignable to void | (() => void)
}
);
這也適用于 useLayoutEffect邑飒,它只在運(yùn)行效果時(shí)有所不同循签。
useContext
useContext 期望一個(gè)上下文對(duì)象,并返回所提供上下文的值疙咸。當(dāng)提供者更新上下文時(shí)县匠,將觸發(fā)重新更新。看一下下面的例子應(yīng)該會(huì)更好的理解:
const ColorContext = React.createContext({ color: "green" });
const Welcome = () => {
const { color } = useContext(ColorContext);
return <div style={{ color }}>Welcome</div>;
};
同樣乞旦,我們不需要對(duì)類型做太多操作贼穆。類型是推斷出來的。
const ColorContext = React.createContext({ color: "green" });
const { color } = useContext(ColorContext);
// const color: string
const UserContext = React.createContext({ id: 1, name: "Test User" });
const { id, name } = useContext(UserContext);
// const id: number
// const name: string
useReducer
有時(shí)我們要處理更復(fù)雜的狀態(tài)兰粉,或者可能需要依賴于之前的狀態(tài)故痊。useReducer 接受一個(gè)函數(shù),該函數(shù)根據(jù)先前的狀態(tài)和操作計(jì)算出對(duì)應(yīng)的狀態(tài)玖姑。下面的示例摘自官方文檔:
const [state, dispatch] = useReducer(reducer, initialArg, init);
如果查看文檔中的示例愕秫,我們會(huì)注意到需要做一些額外的輸入工作,來看看這個(gè)稍作調(diào)整的例子:
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter({initialState = 0}) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
當(dāng)前狀態(tài)無法正確推斷焰络。但是我們可以通過為減速函數(shù)添加類型來改變這一點(diǎn)戴甩。通過在減速函數(shù)中定義狀態(tài)和動(dòng)作,我們現(xiàn)在可以推斷出 useReducer 提供的狀態(tài)闪彼。讓我們修改一下這個(gè)例子甜孤。
type ActionType = {
type: 'increment' | 'decrement';
};
type State = { count: number };
function reducer(state: State, action: ActionType) {
// ...
}
現(xiàn)在我們可以確保在 Counter 中可以推斷出類型:
function Counter({initialState = 0}) {
const [state, dispatch] = useReducer(reducer, initialState);
// const state = State
// ...
}
當(dāng)嘗試分派一個(gè)不存在的類型時(shí),將會(huì)出現(xiàn)一個(gè)錯(cuò)誤备蚓。
dispatch({type: 'increment'}); // Works!
dispatch({type: 'reset'});
// Error! type '"reset"' is not assignable to type '"increment" | "decrement"'
useReducer 也可以在需要時(shí)延遲初始化课蔬,因?yàn)橛袝r(shí)可能需要先計(jì)算初始狀態(tài):
function init(initialCount) {
return {count: initialCount};
}
function Counter({ initialCount = 0 }) {
const [state, dispatch] = useReducer(red, initialCount, init);
// const state: State
// ...
}
從上面的例子中可以看出怕吴,類型是通過一個(gè)延遲初始化的useReducer 來推斷的掉瞳,這是由于正確鍵入了減速函數(shù)雹食。
關(guān)于 useReducer庭瑰,我們不需要知道更多了躁劣。
useCallback
有時(shí)我們需要記錄回調(diào)黔夭。useCallback 接受一個(gè)內(nèi)聯(lián)回調(diào)和一個(gè)輸入數(shù)組醒第,只有當(dāng)其中一個(gè)值發(fā)生更改時(shí)才更新記錄供置。讓我們來看一個(gè)例子:
const add = (a: number, b: number) => a + b;
const memoizedCallback = useCallback(
(a) => {
add(a, b);
},
[b]
);
有趣的是况凉,我們可以調(diào)用 memoizedCallback 的任何類型谚鄙,不會(huì)看到 TypeScript 報(bào)錯(cuò):
memoizedCallback("ok!"); // Works!
memoizedCallback(1); // Works!
在本例中,memoizedCallback 可以處理字符串或數(shù)字刁绒,盡管 add 函數(shù)需要兩個(gè)數(shù)字闷营。要解決這個(gè)問題,我們需要在編寫內(nèi)聯(lián)函數(shù)時(shí)更加具體知市,添加類型傻盟。
const memoizedCallback = useCallback(
(a: number) => {
add(a, b);
},
[b]
);
現(xiàn)在,我們需要傳遞一個(gè)數(shù)字嫂丙,否則編譯器會(huì)報(bào)錯(cuò)娘赴。
memoizedCallback("ok");
// Error! Argument of type '"ok"' is not assignable to argument of type 'number'
memoizedCallback(1); // Works!
useMemo
useMemo 與useCallback 非常相似,但是返回一個(gè)值跟啤,而不是一個(gè)回調(diào)函數(shù)诽表。下面是來自文檔的內(nèi)容唉锌。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
因此,如果我們基于上述構(gòu)建一個(gè)示例竿奏,注意到我們不需要對(duì)類型做任何操作:
function calculate(a: number): number {
// do some calculations here...
}
function runCalculate() {
const calculatedValue = useMemo(() => calculate(a), [a]);
// const calculatedValue : number
}
useRef
最后袄简,我們將再看一個(gè) hook: useRef。
當(dāng)使用 useRef 時(shí)议双,我們可以訪問一個(gè)可變的引用對(duì)象痘番。此外,我們可以將初始值傳遞給 useRef平痰,它用于初始化可變 ref 對(duì)象公開的當(dāng)前屬性汞舱。當(dāng)試圖訪問函數(shù)中的一些組件時(shí),這是很有用的宗雇。
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus(); // Error! Object is possibly 'null'
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
我們可以看到 TypeScript 在報(bào)錯(cuò)昂芜,因?yàn)槲覀冇?null 初始化了useRef,這是一種有效的情況赔蒲,因?yàn)橛袝r(shí)設(shè)置引用可能會(huì)在稍后的時(shí)間點(diǎn)發(fā)生泌神。
這意味著,我們?cè)谑褂?useRef 時(shí)需要更加明確舞虱。
function TextInputWithFocusButton() {
const inputEl = useRef<HTMLInputElement>(null);
const onButtonClick = () => {
inputEl.current.focus(); // Error! Object is possibly 'null'
};
// ...
}
通過定義實(shí)際類型 useRef<HTMLInputElement>
來使用 useRef 時(shí)更加具體欢际,但仍然不能消除錯(cuò)誤。應(yīng)該檢查當(dāng)前屬性是否存在矾兜,损趋;來避免編譯器報(bào)錯(cuò)。
function TextInputWithFocusButton() {
const inputEl = useRef<HTMLInputElement>(null);
const onButtonClick = () => {
if (inputEl.current) {
inputEl.current.focus(); // Works!
}
};
// ...
}
useRef 也可以用作實(shí)例變量椅寺。
如果我們需要能夠更新當(dāng)前屬性浑槽,我們需要使用 useRef 與泛型類型 type| null
:
function sleep() {
const timeoutRefId = useRef<number | null>();
useEffect(() => {
const id = setTimeout(() => {
// ...
});
if (timeoutRefId.current) {
timeoutRefId.current = id;
}
return () => {
if (timeoutRefId.current) {
clearTimeout(timeoutRefId.current);
}
};
});
// ...
}
關(guān)于 React Hooks 還有一些更有趣的東西需要學(xué)習(xí),但不是針對(duì)于TypeScript 的返帕。如果對(duì)此有更多的興趣桐玻,請(qǐng)參考 官方React文檔中關(guān)于Hook的文檔。
對(duì)此荆萤,我們可以很好地理解如何在 Typescript 中使用 React Hooks镊靴。
如果您有任何問題或反饋,請(qǐng)?jiān)谶@里留言或通過 Twitter: A. Sharif聯(lián)系我們链韭。