TS_React:Hook类型化

作家村上春树在《当我谈跑步时我谈些什么》里说道:“Pain is inevitable,but suffering is optional” 即 痛苦不可避免,但我们可以选择不受苦。这个选择权,来自不对抗过去,不担忧未来。

大家好,我是柒八九

在前几天,我们开辟了--TypeScript实战系列,主要讲TSReact中的应用实战。

大家如果对React了解/熟悉的话,想必都听过Hook。在当下的React开发中,函数组件大行其道。而Hook就是为了给函数组件添加内部状态还有处理副作用的。换句话说,Hook已经在现在的React的开发中, 变得不可替代。

而,今天我们就简单的聊聊,如何利用TSHook进行类型化处理。有一点需要特别指出,对hook进行类型化处理,需要利用泛型的语法,如果对泛型没有一个大体的了解,还是需要异步一些常规资料中,先进行简单的学习。

好了,天不早了。我们开始粗发

你能所学到的知识点

React各种hook的类型化处理,总有一款,让你欲罢不能


文章概要

  1. 依赖类型推断
  2. 类型化 useState
  3. 类型化 useReducer
  4. 类型化 useRef
  5. 类型化 forwardRef
  6. 类型化 useEffect 和 useLayoutEffect
  7. 类型化 useMemo 和 useCallback
  8. 类型化 useContext
  9. 类型化自定义hook

1. 依赖类型推断

在绝大部分,TS都可以根据hook中的值来推断它们的类型:也就是我们常说的类型推断

何为类型推断,简单来说:类型推断就是基于赋值表达式推断类型的能⼒ts采用将类型标注声明放在变量之后(即类型后置)的方式来对变量的类型进行标注。而使⽤类型标注后置的好处就是编译器可以通过代码所在的上下⽂推导其对应的类型,⽆须再声明变量类型。

  • 具有初始化值的变量
  • 默认值的函数参数
  • 函数返回的类型

都可以根据上下⽂推断出来。

例如,下面的代码可以在ts环境中正常运行,且能够通过类型推断推导出name的类型为string类型。

const [name, setName] = useState('前端柒八九');

何时不能依赖类型推断

下面的两种情境下,类型推断有点力不从心

  • ts推断出的类型过于宽松
  • 类型推断错误

推断出的类型过于宽松

我们之前的例子--有一个字符串类型的name。但是我们假设这个name只能有两个预定的值中的一个。

在这种情况下,我们会希望name有一个非常具体的类型,例如这个类型。

type Name = '前端柒八九' | '前端工程师' ;

这种类型同时使用联合类型字面类型

在这种情况下,推断的类型过于宽松(是string,而不是我们想要的2个字符串的特定子集),这种情况下就必须自己指定类型。

const [name, setName] = useState<Name>('前端柒八九');

类型推断错误

有时,推断的类型是错误的(或者限制性太强不是你想要的类型)。这种情况经常发生在ReactuseState 默认值中。比方说,name 的初始值是null

const [name, setName] = useState(null);

在这种情况下,TypeScript 会推断出namenull类型的(这意味着它总是null)。这显然是错误的:我们以后会想把 name 设置成一个字符串。

此时你必须告诉 TypeScript,它可以是别的类型。

const [name, setName] = useState<string | null>(null);

通过这样处理后,TypeScript 会正确理解name可以是null也可以是string

这里要提到的一件事是,当类型推断不起作用时,应该依靠泛型参数而不是类型断言

const [name, setName] = useState<Name>('前端柒八九'); 推荐使用

const [name, setName] = useState('前端柒八九' as Name); 不推荐使用


2. 类型化 useState

在文章开头,我们已经通过类型推断讲过了,如何处理useState的各种情况。这里就不在赘述了。

const [name, setName] = useState<string | null>(null);

3. 类型化 useReducer

useReducer 的类型比 useState 要复杂一些。如果看过源码的同学,可能有印象,其实useState就是useReducer简化版

针对useReducer有两样东西要类型化处理:stateaction

这里有一个useReducer的简单例子。针对input做简单的数据收集处理。

import { useReducer } from 'react';

const initialValue = {
  username: '',
  email: '',
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'username':
      return { ...state, username: action.payload };
    case 'email':
      return { ...state, email: action.payload };
    case 'reset':
      return initialValue;
    default:
      throw new Error(`未定义的action: ${action.type}`);
  }
};

const Form = () => {
  const [state, dispatch] = useReducer(reducer, initialValue);
  return (
    <div>
      <input
        type="text"
        value={state.username}
        onChange={(event) =>
          dispatch({ type: 'username', payload: event.target.value })
        }
      />
      <input
        type="email"
        value={state.email}
        onChange={(event) =>
          dispatch({ type: 'email', payload: event.target.value })
        }
      />
    </div>
  );
};

export default Form;

类型化 reducer 的state

我们有两个选择来类型化reducer-state

  • 使用初始值(如果有的话)和 typeof 操作符
  • 使用类型别名

使用typeof 操作符

const initialValue = {
  username: '',
  email: '',
};

+ const reducer = (state: typeof initialValue, action) => {
    switch (action.type) {
      case 'username':
        return {...state,  username: action.payload };
      case 'email':
        return {...state,  email: action.payload };
      case 'reset':
        return initialValue;
      default:
        throw new Error(`未定义的action: ${action.type}`);
    }
};

使用类型别名

+type State = {
+  username: string;
+  email: string;
+};

const initialValue = {
  username: '',
  email: '',
};

+ const reducer = (state: State, action) => {
    switch (action.type) {
      case 'username':
        return { ...state, username: action.payload };
      case 'email':
        return { ...state, email: action.payload };
      case 'reset':
        return initialValue;
      default:
        throw new Error(`未定义的action: ${action.type}`);
    }
};

类型化 reducer 的action

reducer-action的类型比reducer-state要难一点,因为它的结构会根据具体的action而改变。

例如,对于 username-action,我们可能期望有以下类型。

type UsernameAction = {
  type: 'username';
  payload: string;
};

但对于 reset-action,我们不需要payload字段。

type ResetAction = {
  type: 'reset';
};

我们可以借助联合类型区别对待不同的action

const initialValue = {
  username: "",
  email: ""
};

+type Action =
+  | { type: "username"; payload: string }
+  | { type: "email"; payload: string }
+  | { type: "reset" };

+ const reducer = (state: typeof initialValue, action: Action) => {
    switch (action.type) {
      case "username":
        return {...state, username: action.payload };
      case "email":
        return { ...state, email: action.payload };
      case "reset":
        return initialValue;
      default:
        throw new Error(`未定义的action: ${action.type}`);
    }
};

Action类型表示的是,它可以接受联合类型中包含的三种类型中的任何一种。因此,如果 TypeScript 看到 action.typeusername,它就会自动知道它应该是第一种情况,并且payload应该是一个string

通过对state/action类型化后,useReducer能够从reducer函数的type中推断出它需要的一切。

下面是整体的代码。(省略了,jsx部分)

import { useReducer } from 'react';

const initialValue = {
  username: '',
  email: '',
};

+type Action =
+  | { type: 'username'; payload: string }
+  | { type: 'email'; payload: string }
+  | { type: 'reset' };

+const reducer = (state: typeof initialValue, action: Action) => {
  switch (action.type) {
    case 'username':
      return { ...state, username: action.payload };
    case 'email':
      return { ...state, email: action.payload };
    case 'reset':
      return initialValue;
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
};

const Form = () => {
  const [state, dispatch] = useReducer(reducer, initialValue);

  return (
   ...省略了..
  );
};

export default Form;

4. 类型化 useRef

useRef 有两个主要用途

  • 保存一个自定义的可变值(它的值变更不会触发更新)。
  • 保持对一个DOM对象的引用

类型化可变值

它基本上与 useState 相同。想让useRef保存一个自定义的值,你需要告诉它这个类型。

function Timer() {
+  const intervalRef = useRef<number | undefined>();

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });

  // ...
}

类型化 DOM 节点

在DOM节点上使用useRef的一个经典用例是处理input元素的focus

mport { useRef, useEffect } from 'react';

const AutoFocusInput = () => {
+  const inputRef = useRef(null);

  useEffect(() => {
+    inputRef.current.focus();
  }, []);

+  return <input ref={inputRef} type="text" value="前端柒八九" />;
};

export default AutoFocusInput;

TypeScript内置的DOM元素类型。这些类型的结构总是相同的:

如果name是你正在使用的HTML标签的名称,相应的类型将是HTMLNameElement

这里有几个特例

  • <a>标签的类型为HTMLAnchorElement
  • <h1>标签的类型为HTMLHeadingElement

对于<input>,该类型的名称将是HTMLInputElement

mport { useRef, useEffect } from 'react';

const AutoFocusInput = () => {
+  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
+    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} type="text" value="前端柒八九" />;
};

export default AutoFocusInput;

注意:在inputRef.current?.focus()上加了一个?。这是因为对于 TypeScriptinputRef.current可能是空的。在这种情况下,我们知道它不会是空的,因为它是在 useEffect 第一次运行之前由 React 填充的。


5. 类型化 forwardRef

有时想把ref转发给子组件。要做到这一点,在 React 中我们必须用 forwardRef包装组件

import { ChangeEvent } from 'react';

type Props = {
  value: string,
  handleChange: (event: ChangeEvent<HTMLInputElement>) => void,
};

const TextInput = ({ value, handleChange }: Props) => {
  return <input type="text" value={value} onChange={handleChange} />;
};

例如,存在一个组件TextInput而我们想在父组件的调用处,通过ref来控制子组件input

此时,就需要用forwardRef来处理。

import { forwardRef, ChangeEvent } from 'react';

type Props = {
  value: string;
  handleChange: (event: ChangeEvent<HTMLInputElement>) => void;
};

+const TextInput = forwardRef<HTMLInputElement, Props>(
+  ({ value, handleChange }, ref) => {
    return (
+      <input ref={ref} type="text" value={value} onChange={handleChange} />
    );
  }
);

此语法只需要向 forwardRef 提供它应该期待的HTMLElement(在这种情况下是HTMLInputElement)。

有一点,需要指出:组件参数refprops的顺序与泛型的<HTMLInputElement, Props>不一样。


6. 类型化 useEffect 和 useLayoutEffect

你不必给他们任何类型

唯一需要注意的是隐式返回useEffect里面的回调应该是什么都不返回,或者是一个会清理任何副作用的Destructor函数(析构函数,这个词借用了C++中类的说法)


7. 类型化 useMemo 和 useCallback

你不必给他们任何类型


8. 类型化 useContext

context提供类型是非常容易的。首先,为context创建一个类型,然后把它作为一个泛型提供给createContext函数。

import React, { createContext, useEffect, useState, ReactNode } from 'react';

+type User = {
+  name: string;
+  email: string;
+  freeTrial: boolean;
+};

+type AuthValue = {
+  user: User | null;
+  signOut: () => void;
+};

+const AuthContext = createContext<AuthValue | undefined>(undefined);

type Props = {
  children: ReactNode;
};

const AuthContextProvider = ({ children }: Props) => {
  const [user, setUser] = useState(null);

  const signOut = () => {
    setUser(null);
  };

  useEffect(() => {
    // 副作用处理
  }, []);

  return (
+    <AuthContext.Provider value={{ user, signOut }}>
      {children}
+    </AuthContext.Provider>
  );
};

export default AuthContextProvider;

一旦你向createContext提供了泛型,剩余的事,都由ts为你代劳。

上述实现的一个问题是,就TypeScript而言,context的值可以是未定义的。也就是在我们使用context的值的时候,可能取不到。此时,ts可能会阻拦代码的编译。

如何解决context的值可能是未定义的情况呢。我们针对context的获取可以使用一个自定义的hook

export const useAuthContext = () => {
  const context = useContext(AuthContext);

  if (context === undefined) {
    throw new Error('useAuthContext必须在AuthContext上下文中使用');
  }

  return context;
};

通过类型保护,使得我们在使用context的时候,总是有值的。


9. 类型化自定义hook

类型化自定义hook基本上和类型化普通函数一样

针对如何类型化普通函数,在一些教程中很多,一搜一大把。这里也不过多描述。

我们来看一个比较有意思的例子。有一个自定义hook,它想要返回一个元祖。

const useCustomHook = () => {
  return ['abc', 123];
};

TypeScipt 将扩大 useCustomHook 的返回类型为(number | string)[](一个可以包含数字或字符串的数组)。显然,这不是你想要的,你想要的是第一个参数总是一个字符串,第二个例子总是一个数字。

所以,这种情况下,我们可以利用泛型对返回类型做一个限制处理。

const useCustomHook = (): [string, number] => {
  return ['abc', 123];
};


后记

分享是一种态度

参考资料:

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容