翻譯:在 React Hooks 中使用 Typescript 小記

在 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

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)系我們链韭。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末邑闲,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子梧油,更是在濱河造成了極大的恐慌,老刑警劉巖州邢,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件儡陨,死亡現(xiàn)場(chǎng)離奇詭異褪子,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)骗村,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門嫌褪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人胚股,你說我怎么就攤上這事笼痛。” “怎么了琅拌?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵缨伊,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我进宝,道長(zhǎng)刻坊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任党晋,我火速辦了婚禮谭胚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘未玻。我一直安慰自己灾而,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布扳剿。 她就那樣靜靜地躺著旁趟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪舞终。 梳的紋絲不亂的頭發(fā)上轻庆,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音敛劝,去河邊找鬼余爆。 笑死,一個(gè)胖子當(dāng)著我的面吹牛夸盟,可吹牛的內(nèi)容都是我干的蛾方。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼上陕,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼桩砰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起释簿,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤亚隅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后庶溶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體煮纵,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡懂鸵,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了行疏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匆光。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖酿联,靈堂內(nèi)的尸體忽然破棺而出终息,到底是詐尸還是另有隱情,我是刑警寧澤贞让,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布周崭,位于F島的核電站,受9級(jí)特大地震影響震桶,放射性物質(zhì)發(fā)生泄漏休傍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一蹲姐、第九天 我趴在偏房一處隱蔽的房頂上張望磨取。 院中可真熱鬧,春花似錦柴墩、人聲如沸忙厌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逢净。三九已至,卻和暖如春歼指,著一層夾襖步出監(jiān)牢的瞬間爹土,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工踩身, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胀茵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓挟阻,卻偏偏與公主長(zhǎng)得像琼娘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子附鸽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • React是現(xiàn)在最流行的前端框架之一脱拼,它的輕量化,組件化坷备,單向數(shù)據(jù)流等特性把前端引入了一個(gè)新的高度熄浓,現(xiàn)在它又引入的...
    老鼠AI大米_Java全棧閱讀 5,775評(píng)論 0 26
  • 原文鏈接:https://www.v2ex.com/t/570176#reply10 React Hooks 是什...
    勿忘巛心安閱讀 1,399評(píng)論 0 3
  • 如果你之前對(duì)于Hooks沒有了解,那么你可能需要看下概述部分省撑。你或許也可以在一些常見的問題中找到有用的信息玉组。 基本...
    xiaohesong閱讀 21,081評(píng)論 4 11
  • 元宵節(jié)到了谎柄,我和家人們一起吃元宵,我們先把元宵倒進(jìn)鍋里惯雳,然后我們就等著元宵熟了再吃,過了一會(huì)兒鸿摇,媽媽就端著幾碗熱氣...
    王玉瀅閱讀 234評(píng)論 0 1
  • 志明在唱歌的同時(shí)石景,程皓已經(jīng)抱起箱子開始讓觀眾打賞,這時(shí)已有人往箱子里扔錢拙吉,程皓不停地向觀眾說著謝謝潮孽。 待田志明這一...
    小新順閱讀 311評(píng)論 1 12