翻译:在 React Hooks 中使用 Typescript 小记

在 React Hooks 中使用 Typescript 小记

最近在关注 Typescript 和 react hook 相关的知识,看到了这篇文章,还不错,get 到了。感谢作者的分享。

原文:Notes on TypeScript: React Hooks

原文作者简介:A. Sharif:专注于质量。软件开发。产品管理。

https://twitter.com/sharifsbeat

@busypeoples

busypeoples

简介

这些笔记有助于更好地理解 TypeScript,并且在某些特定情况下如何使用 TypeScript 也会有帮助。所有示例都基于TypeScript 3.2。

React Hooks

在 “Notes on TypeScript"(Typescript 小记) 系列的这一部分中,我们将了解如何在 React Hooks 中使用 TypeScript,并了解更多关于React Hooks 的知识。

我们将参考官方 React 文档关于 Hook 的文档,当需要了解更多关于 hook 的信息或需要特定问题的特定答案时,这是一个非常有价值的资源。

在一般情况下,在16.8中已经添加了 Hook 来进行响应,并使开发人员能够在函数组件中使用 State (状态),在此之前,只有在类组件中才可能使用 State (状态)。文档说明有一些基本的 Hook API 和其他的 Hook API。

基本的 Hooks 有 useState, useEffect, useContext,还有一些其他的,比如useReducer, useCallback, useMemo, useRef.

useState

让我们从 useState 开始,这是一个基本的 hook,见名知义,它应该用于状态处理。

const [state, setState] = useState(initialState);

查看上面的示例,我们可以看到 useState 返回一个状态值以及一个更新它的函数。但我们如何输入 state 和 setState 呢?

有趣的是,TypeScript 可以推断类型,这意味着通过定义initialState,可以推断状态值和更新函数的类型。

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[]

上面的例子很好地说明,我们不需要进行任何手工输入。但如果没有初始状态呢?当尝试更新状态时,上面的示例会中断。

我们可以在需要时使用 useState 手动定义类型。

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 的更新函数需要返回完整的状态。

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!

另一件值得注意的有趣的事情是,我们可以通过将函数传递给useState 来惰性地设置状态。

const [state, setState] = useState(() => {
  props.init + 1;
});

// const state: number

同样,TypeScript 可以推断状态类型。

这意味着我们在使用 useState 时不需要做太多工作。只有在没有初始值的情况下才需要加入类型限制,因为有初始值时可以推断出实际状态的类型。

useEffect

另一个基本的钩子是 useEffect,它在处理副作用时非常有用,比如日志记录、突变或订阅事件侦听器。useEffect 可以传递一个函数,运行这个函数可以执行一些清除功能,比如清除订阅信息或者监听器,这是非常有用的。此外 useEffect 提供了第二个参数,包含一组值,确保传递给 useEffect 的函数,只有当这个值改变了,函数才会执行。这让我们可以控制何时运行函数处理副作用。

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source]
);

以文档中的原始示例为例,我们可以注意到在使用 useEffect 时不需要任何额外的类型。

当我们试图返回不是函数或 effect 函数中未定义的内容时,TypeScript 会发出报错信息。

useEffect(
  () => {
    subscribe();
    return null; // Error! Type 'null' is not assignable to void | (() => void)
  }
);

这也适用于 useLayoutEffect,它只在运行效果时有所不同。

useContext

useContext 期望一个上下文对象,并返回所提供上下文的值。当提供者更新上下文时,将触发重新更新。看一下下面的例子应该会更好的理解:

const ColorContext = React.createContext({ color: "green" });

const Welcome = () => {
  const { color } = useContext(ColorContext);
  return <div style={{ color }}>Welcome</div>;
};

同样,我们不需要对类型做太多操作。类型是推断出来的。

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

有时我们要处理更复杂的状态,或者可能需要依赖于之前的状态。useReducer 接受一个函数,该函数根据先前的状态和操作计算出对应的状态。下面的示例摘自官方文档:

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

如果查看文档中的示例,我们会注意到需要做一些额外的输入工作,来看看这个稍作调整的例子:

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>
    </>
  );
}

当前状态无法正确推断。但是我们可以通过为减速函数添加类型来改变这一点。通过在减速函数中定义状态和动作,我们现在可以推断出 useReducer 提供的状态。让我们修改一下这个例子。

type ActionType = {
  type: 'increment' | 'decrement';
};
type State = { count: number };
function reducer(state: State, action: ActionType) {
  // ...
}

现在我们可以确保在 Counter 中可以推断出类型:

function Counter({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, initialState);
  // const state = State
  // ...
}

当尝试分派一个不存在的类型时,将会出现一个错误。

dispatch({type: 'increment'}); // Works!
dispatch({type: 'reset'});
// Error! type '"reset"' is not assignable to type '"increment" | "decrement"'

useReducer 也可以在需要时延迟初始化,因为有时可能需要先计算初始状态:

function init(initialCount) {
  return {count: initialCount};
}

function Counter({ initialCount = 0 }) {
  const [state, dispatch] = useReducer(red, initialCount, init);
  // const state: State
  // ...
}

从上面的例子中可以看出,类型是通过一个延迟初始化的useReducer 来推断的,这是由于正确键入了减速函数。

关于 useReducer,我们不需要知道更多了。

useCallback

有时我们需要记录回调。useCallback 接受一个内联回调和一个输入数组,只有当其中一个值发生更改时才更新记录。让我们来看一个例子:

const add = (a: number, b: number) => a + b;
const memoizedCallback = useCallback(
  (a) => {
    add(a, b);
  },
  [b]
);

有趣的是,我们可以调用 memoizedCallback 的任何类型,不会看到 TypeScript 报错:

memoizedCallback("ok!"); // Works!
memoizedCallback(1); // Works!

在本例中,memoizedCallback 可以处理字符串或数字,尽管 add 函数需要两个数字。要解决这个问题,我们需要在编写内联函数时更加具体,添加类型。

const memoizedCallback = useCallback(
  (a: number) => {
    add(a, b);
  },
  [b]
);

现在,我们需要传递一个数字,否则编译器会报错。

memoizedCallback("ok");
// Error! Argument of type '"ok"' is not assignable to argument of type 'number'
memoizedCallback(1); // Works!

useMemo

useMemo 与useCallback 非常相似,但是返回一个值,而不是一个回调函数。下面是来自文档的内容。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

因此,如果我们基于上述构建一个示例,注意到我们不需要对类型做任何操作:

function calculate(a: number): number {
  // do some calculations here...
}

function runCalculate() {
  const calculatedValue =  useMemo(() => calculate(a), [a]);
  // const calculatedValue : number
}

useRef

最后,我们将再看一个 hook: useRef。

当使用 useRef 时,我们可以访问一个可变的引用对象。此外,我们可以将初始值传递给 useRef,它用于初始化可变 ref 对象公开的当前属性。当试图访问函数中的一些组件时,这是很有用的。

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 在报错,因为我们用 null 初始化了useRef,这是一种有效的情况,因为有时设置引用可能会在稍后的时间点发生。

这意味着,我们在使用 useRef 时需要更加明确。

function TextInputWithFocusButton() {
  const inputEl = useRef<HTMLInputElement>(null);
  const onButtonClick = () => {
    inputEl.current.focus(); // Error! Object is possibly 'null'
  };
  // ...
}

通过定义实际类型 useRef<HTMLInputElement> 来使用 useRef 时更加具体,但仍然不能消除错误。应该检查当前属性是否存在,;来避免编译器报错。

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

useRef 也可以用作实例变量。

如果我们需要能够更新当前属性,我们需要使用 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);
      }
    };
  });
  // ...
}

关于 React Hooks 还有一些更有趣的东西需要学习,但不是针对于TypeScript 的。如果对此有更多的兴趣,请参考 官方React文档中关于Hook的文档

对此,我们可以很好地理解如何在 Typescript 中使用 React Hooks。

如果您有任何问题或反馈,请在这里留言或通过 Twitter: A. Sharif联系我们。

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

推荐阅读更多精彩内容

  • React是现在最流行的前端框架之一,它的轻量化,组件化,单向数据流等特性把前端引入了一个新的高度,现在它又引入的...
    老鼠AI大米_Java全栈阅读 5,780评论 0 26
  • 原文链接:https://www.v2ex.com/t/570176#reply10 React Hooks 是什...
    勿忘巛心安阅读 1,401评论 0 3
  • 如果你之前对于Hooks没有了解,那么你可能需要看下概述部分。你或许也可以在一些常见的问题中找到有用的信息。 基本...
    xiaohesong阅读 21,081评论 4 11
  • 元宵节到了,我和家人们一起吃元宵,我们先把元宵倒进锅里,然后我们就等着元宵熟了再吃,过了一会儿,妈妈就端着几碗热气...
    王玉滢阅读 234评论 0 1
  • 志明在唱歌的同时,程皓已经抱起箱子开始让观众打赏,这时已有人往箱子里扔钱,程皓不停地向观众说着谢谢。 待田志明这一...
    小新顺阅读 311评论 1 12