useTypescript-React Hooks和TypeScript徹底指南

引言

React v16.8 引入了 Hooks,它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。這些功能能夠在應用程序中的各個組件之間使用,從而易於共享邏輯。Hook 使人興奮並迅速被採用,React 團隊甚至想象它們最終將替換類組件。css

之前在 React 中,共享邏輯的方法是經過高階組件和 props 渲染。Hooks 提供了一種更簡單方便的方法來重用代碼並使組件可塑形更強。html

本文將展現 TypeScript 與 React 集成後的一些變化,以及如何將類型添加到 Hooks 以及你的自定義 Hooks 上。
前端

引入 Typescript 後的變化

有狀態組件(ClassComponent)

API 對應爲:react

React.Component<P, S>

class MyComponent extends React.Component<Props, State> { ...

如下是官網的一個例子,建立 Props 和 State 接口,Props 接口接受 name 和 enthusiasmLevel 參數,State 接口接受 currentEnthusiasm 參數:git

import * as React from "react";

export interface Props {
  name: string;
  enthusiasmLevel?: number;
}

interface State {
  currentEnthusiasm: number;
}

class Hello extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { currentEnthusiasm: props.enthusiasmLevel || 1 };
  }

  onIncrement = () => this.updateEnthusiasm(this.state.currentEnthusiasm + 1);
  onDecrement = () => this.updateEnthusiasm(this.state.currentEnthusiasm - 1);

  render() {
    const { name } = this.props;

    if (this.state.currentEnthusiasm <= 0) {
      throw new Error('You could be a little more enthusiastic. :D');
    }

    return (
      <div className="hello">
        <div className="greeting">
          Hello {name + getExclamationMarks(this.state.currentEnthusiasm)}
        </div>
        <button onClick={this.onDecrement}>-</button>
        <button onClick={this.onIncrement}>+</button>
      </div>
    );
  }

  updateEnthusiasm(currentEnthusiasm: number) {
    this.setState({ currentEnthusiasm });
  }
}

export default Hello;

function getExclamationMarks(numChars: number) {
  return Array(numChars + 1).join('!');
}

TypeScript 能夠對 JSX 進行解析,充分利用其自己的靜態檢查功能,使用泛型進行 Props、 State 的類型定義。定義後在使用 this.state 和 this.props 時能夠在編輯器中得到更好的智能提示,而且會對類型進行檢查。github

react 規定不能經過 this.props.xxx 和 this.state.xxx 直接進行修改,因此能夠經過 readonly 將 State 和 Props 標記爲不可變數據:typescript

interface Props {
  readonly number: number;
}

interface State {
  readonly color: string;
}

export class Hello extends React.Component<Props, State> {
  someMethod() {
    this.props.number = 123; // Error: props 是不可變的
    this.state.color = 'red'; // Error: 你應該使用 this.setState()
  }
}

無狀態組件(StatelessComponent)

API 對應爲:api

// SFC: stateless function components
const List: React.SFC<IProps> = props => null
// v16.8起,因爲hooks的加入,函數式組件也可使用state,因此這個命名不許確。新的react聲明文件裏,也定義了React.FC類型^_^
React.FunctionComponent<P> or React.FC<P>。

const MyComponent: React.FC<Props> = ...

無狀態組件也稱爲傻瓜組件,若是一個組件內部沒有自身的 state,那麼組件就能夠稱爲無狀態組件。在@types/react已經定義了一個類型type SFC<P = {}> = StatelessComponent數組

先看一下以前無狀態組件的寫法:瀏覽器

import React from 'react'

const Button = ({ onClick: handleClick, children }) => (
  <button onClick={handleClick}>{children}</button>
)

若是採用 ts 來編寫出來的無狀態組件是這樣的:

import React, { MouseEvent, SFC } from 'react';

type Props = { onClick(e: MouseEvent<HTMLElement>): void };

const Button: SFC<Props> = ({ onClick: handleClick, children }) => (
  <button onClick={handleClick}>{children}</button>
);

事件處理

咱們在進行事件註冊時常常會在事件處理函數中使用 event 事件對象,例如當使用鼠標事件時咱們會經過 clientX、clientY 去獲取指針的座標。

你們能夠想到直接把 event 設置爲 any 類型,可是這樣就失去了咱們對代碼進行靜態檢查的意義。

function handleMouseChange (event: any) {
  console.log(event.clientY)
}

試想下當咱們註冊一個 Touch 事件,而後錯誤的經過事件處理函數中的 event 對象去獲取其 clientY 屬性的值,在這裏咱們已經將 event 設置爲 any 類型,致使 TypeScript 在編譯時並不會提示咱們錯誤, 當咱們經過 event.clientY 訪問時就有問題了,由於 Touch 事件的 event 對象並無 clientY 這個屬性。

經過 interface 對 event 對象進行類型聲明編寫的話又十分浪費時間,幸運的是 React 的聲明文件提供了 Event 對象的類型聲明。

  • 通用的 React Event Handler

API 對應爲:

React.ReactEventHandler<HTMLElement>

簡單的示例:

const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... }

<input onChange={handleChange} ... />
  • 特殊的 React Event Handler

經常使用 Event 事件對象類型:

ClipboardEvent<T = Element> 剪貼板事件對象


DragEvent<T = Element> 拖拽事件對象


ChangeEvent<T = Element>  Change 事件對象


KeyboardEvent<T = Element> 鍵盤事件對象


MouseEvent<T = Element> 鼠標事件對象


TouchEvent<T = Element>  觸摸事件對象


WheelEvent<T = Element> 滾輪事件對象


AnimationEvent<T = Element> 動畫事件對象


TransitionEvent<T = Element> 過渡事件對象

簡單的示例:

const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... }

<div onMouseMove={handleChange} ... />

React 元素

API 對應爲:

React.ReactElement<P> or JSX.Element

簡單的示例:

// 表示React元素概念的類型: DOM元素組件或用戶定義的複合組件
const elementOnly: React.ReactElement = <div /> || <MyComponent />;

React Node

API 對應爲:

React.ReactNode

表示任何類型的 React 節點(基本上是 ReactElement + 原始 JS 類型的合集)

簡單的示例:

const elementOrComponent: React.ReactNode = 'string' || 0 || false || null || undefined || <div /> || <MyComponent />;

React CSS 屬性

API 對應爲:

React.CSSProperties

用於標識 jsx 文件中的 style 對象(一般用於 css-in-js

簡單的示例:

const styles: React.CSSProperties = { display: 'flex', ...
const element = <div style={styles} ...

Hooks 登場

首先,什麼是 Hooks 呢?

React 一直都提倡使用函數組件,可是有時候須要使用 state 或者其餘一些功能時,只能使用類組件,由於函數組件沒有實例,沒有生命週期函數,只有類組件纔有。

Hooks 是 React 16.8 新增的特性,它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。

默認狀況下,React 包含 10 個鉤子。其中 3 個掛鉤被視爲是最常使用的「基本」或核心掛鉤。還有 7 個額外的「高級」掛鉤,這些掛鉤最經常使用於邊緣狀況。10 個鉤子以下:

  • 基礎

    • useState
    • useEffect
    • useContext
  • 高級

    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue

useState with TypeScript

API 對應爲:

// 傳入惟一的參數: initialState,能夠是數字,字符串等,也能夠是對象或者數組。
// 返回的是包含兩個元素的數組:第一個元素,state 變量,setState 修改 state值的方法。
const [state, setState] = useState(initialState);

useState是一個容許咱們替換類組件中的 this.state 的掛鉤。咱們執行該掛鉤,該掛鉤返回一個包含當前狀態值和一個用於更新狀態的函數的數組。狀態更新時,它會致使組件的從新 render。下面的代碼顯示了一個簡單的 useState 鉤子:

import * as React from 'react';

const MyComponent: React.FC = () => {
  const [count, setCount] = React.useState(0);
  return (
    <div onClick={() => setCount(count + 1)}>
      {count}
    </div>
  );
};

useEffect with TypeScript

API 對應爲:

// 兩個參數
// 第一個是一個函數,是在第一次渲染(componentDidMount)以及以後更新渲染以後會進行的反作用。這個函數可能會有返回值,假若有返回值,返回值也必須是一個函數,會在組件被銷燬(componentWillUnmount)時執行。
// 第二個參數是可選的,是一個數組,數組中存放的是第一個函數中使用的某些反作用屬性。用來優化 useEffect
useEffect(() => { // 須要在componentDidMount執行的內容 return function cleanup() { // 須要在componentWillUnmount執行的內容 } }, [])

useEffect是用於咱們管理反作用(例如 API 調用)並在組件中使用 React 生命週期的。useEffect 將回調函數做爲其參數,而且回調函數能夠返回一個清除函數(cleanup)。回調將在第一次渲染(componentDidMount) 和組件更新時(componentDidUpate)內執行,清理函數將組件被銷燬(componentWillUnmount)內執行。

useEffect(() => {
  // 給 window 綁定點擊事件
  window.addEventListener('click', handleClick);

  return () => {
      // 給 window 移除點擊事件
      window.addEventListener('click', handleClick);
  }
});

默認狀況下,useEffect 將在每一個渲染時被調用,可是你還能夠傳遞一個可選的第二個參數,該參數僅容許您在 useEffect 依賴的值更改時或僅在初始渲染時執行。第二個可選參數是一個數組,僅當其中一個值更改時纔會 reRender(從新渲染)。若是數組爲空,useEffect 將僅在 initial render(初始渲染)時調用。

useEffect(() => {
  // 使用瀏覽器API更新文檔標題
  document.title = `You clicked ${count} times`;
}, [count]);    // 只有當數組中 count 值發生變化時,纔會執行這個useEffect。

useContext with TypeScript

useContext容許您利用React context這樣一種管理應用程序狀態的全局方法,能夠在任何組件內部進行訪問而無需將值傳遞爲 props。

useContext 函數接受一個 Context 對象並返回當前上下文值。當提供程序更新時,此掛鉤將觸發使用最新上下文值的從新渲染。

import { createContext, useContext } from 'react';

props ITheme {
  backgroundColor: string;
  color: string;
}

const ThemeContext = createContext<ITheme>({
  backgroundColor: 'black',
  color: 'white',
})

const themeContext = useContext<ITheme>(ThemeContext);

useReducer with TypeScript

對於更復雜的狀態,您能夠選擇將該 useReducer 函數用做的替代 useState。

const [state,dispatch] =  useReducer(reducer,initialState,init);

若是您之前使用過Redux,則應該很熟悉。useReducer接受 3 個參數(reducer,initialState,init)並返回當前的 state 以及與其配套的 dispatch 方法。reducer 是以下形式的函數(state, action) => newState;initialState 是一個 JavaScript 對象;而 init 參數是一個惰性初始化函數,可讓你延遲加載初始狀態。

這聽起來可能有點抽象,讓咱們看一個實際的例子:

const initialState = 0;
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {number: state.number + 1};
    case 'decrement':
      return {number: state.number - 1};
    default:
      throw new Error();
  }
}
function init(initialState){
    return {number:initialState};
}
function Counter(){
    const [state, dispatch] = useReducer(reducer, initialState,init);
    return (
        <>
          Count: {state.number}
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    )
}

看完例子再結合上面 useReducer 的 api 是否是立馬就明白了呢?

useCallback with TypeScript

useCallback 鉤子返回一個 memoized 回調。這個鉤子函數有兩個參數:第一個參數是一個內聯回調函數,第二個參數是一個數組。數組將在回調函數中引用,並按它們在數組中的存在順序進行訪問。

const memoizedCallback =  useCallback(()=> {
    doSomething(a,b);
  },[ a,b ],);

useCallback 將返回一個記憶化的回調版本,它僅會在某個依賴項改變時才從新計算 memoized 值。當您將回調函數傳遞給子組件時,將使用此鉤子。這將防止沒必要要的渲染,由於僅在值更改時才執行回調,從而能夠優化組件。能夠將這個掛鉤視爲與shouldComponentUpdate生命週期方法相似的概念。

useMemo with TypeScript

useMemo返回一個 memoized 值。 傳遞「建立」函數和依賴項數組。useMemo 只會在其中一個依賴項發生更改時從新計算 memoized 值。此優化有助於避免在每一個渲染上進行昂貴的計算。

const memoizedValue =  useMemo(() =>  computeExpensiveValue( a, b),[ a, b ]);
useMemo 在渲染過程當中傳遞的函數會運行。不要作那些在渲染時一般不會作的事情。例如,反作用屬於 useEffect,而不是 useMemo。

看到這,你可能會以爲,useMemouseCallback的做用有點像啊,那它們之間有什麼區別呢?

  • useCallback 和 useMemo 均可緩存函數的引用或值。
  • 從更細的使用角度來講 useCallback 緩存函數的引用,useMemo 緩存計算數據的值。

useRef with TypeScript

useRef掛鉤容許你建立一個 ref 而且容許你訪問基礎 DOM 節點的屬性。當你須要從元素中提取值或獲取與 DOM 相關的元素信息(例如其滾動位置)時,可使用此方法。

const refContainer  =  useRef(initialValue);

useRef 返回一個可變的 ref 對象,其.current屬性被初始化爲傳遞的參數(initialValue)。返回的對象將存留在整個組件的生命週期中。

function TextInputWithFocusButton() {
  const inputEl = useRef<HTMLInputElement>(null);
  const onButtonClick = () => {
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useImperativeHandle with TypeScript

useImperativeHandle可讓你在使用 ref 時,自定義暴露給父組件的實例值。

useImperativeHandle(ref, createHandle, [inputs])

useImperativeHandle 鉤子函數接受 3 個參數: 一個 React ref、一個 createHandle 函數和一個用於暴露給父組件參數的可選數組。

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = React.forwardRef(FancyInput);

const fancyInputRef = React.createRef();
<FancyInput ref={fancyInputRef}>Click me!</FancyInput>;

useLayoutEffect with TypeScript

與 useEffect Hooks 相似,都是執行反作用操做。可是它是在全部 DOM 更新完成後觸發。能夠用來執行一些與佈局相關的反作用,好比獲取 DOM 元素寬高,窗口滾動距離等等。

useLayoutEffect(() => { doSomething });
進行反作用操做時儘可能優先選擇 useEffect,以避免阻止視圖更新。與 DOM 無關的反作用操做請使用 useEffect。
import React, { useRef, useState, useLayoutEffect } from 'react';

export default () => {

    const divRef = useRef(null);

    const [height, setHeight] = useState(50);

    useLayoutEffect(() => {
        // DOM 更新完成後打印出 div 的高度
        console.log('useLayoutEffect: ', divRef.current.clientHeight);
    })

    return <>
        <div ref={ divRef } style={{ background: 'red', height: height }}>Hello</div>
        <button onClick={ () => setHeight(height + 50) }>改變 div 高度</button>
    </>

}

useDebugValue with TypeScript

useDebugValue是用於調試自定義掛鉤(自定義掛鉤請參考https://reactjs.org/docs/hooks-custom.html)的工具。它容許您在 React Dev Tools 中顯示自定義鉤子函數的標籤。

示例

我以前基於 umi+react+typescript+ant-design 構建了一個簡單的中後臺通用模板。

涵蓋的功能以下:

- 組件
  - 基礎表格
  - ECharts 圖表
  - 表單
    - 基礎表單
    - 分步表單
  - 編輯器

- 控制檯
- 錯誤頁面
  - 404

裏面對於在 react 中結合Hooks使用 typescript 的各類場景都有很好的實踐,你們感興趣的能夠參考一下,https://github.com/FSFED/Umi-hooks/tree/feature_hook,固然不要吝惜你的 star!!!

最後

你能夠關注個人同名公衆號【前端森林】,這裏我會按期發一些大前端相關的前沿文章和平常開發過程當中的實戰總結。固然,我也是開源社區的積極貢獻者,github地址https://github.com/Jack-cool,歡迎star!!!

相關文章
相關標籤/搜索