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

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

0.722019.06.15 19:56:12字数 1,949阅读 7,462

在 React Hooks 中使用 Typescript 小记

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

原文:Notes on TypeScript: React Hooks

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

https://twitter.com/sharifsbeat

@busypeoples

 
github

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 有 useStateuseEffectuseContext,还有一些其他的,比如useReduceruseCallbackuseMemouseRef.

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联系我们。

posted on 2021-12-08 19:02  漫思  阅读(92)  评论(0编辑  收藏  举报

导航