关于 TypeScript 的注释:React Hooks

2025-06-04

关于 TypeScript 的注释:React Hooks

介绍

这些说明应该有助于更好地理解,TypeScript并且在需要查找如何在特定情况下利用 TypeScript 时可能会有所帮助。所有示例均基于 TypeScript 3.2。

React Hooks

在“TypeScript 笔记”系列的这一部分中,我们将了解如何使用 TypeScript 对 React Hooks 进行类型化,并在此过程中了解有关 Hooks 的更多信息。

我们将参考React 官方文档中关于 Hook 的内容,如果您需要了解更多关于 Hook 的知识或需要针对特定​​问题的具体解决方案,那么
这份文档将是一个非常宝贵的资源。通常,Hook 已于 16.8 版本添加到 React 中,使开发者能够在函数组件中使用状态,而在此之前,这只能在类组件中使用。文档指出,Hook 分为基本 Hook 和附加 Hook。
基本 Hook 包括useStateuseEffectuseContext附加 Hook 包括useReduceruseCallbackuseMemouseRef

useState

让我们从基本的钩子开始useState,顾名思义,它应该用于状态处理。

const [state, setState] = useState(initialState);
Enter fullscreen mode Exit fullscreen mode

从上面的例子可以看出,它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[]
Enter fullscreen mode Exit fullscreen mode

上面的例子很好地证明了我们不需要进行任何手动输入。但是如果我们没有初始状态怎么办?上面的示例在尝试更新状态时会崩溃。
我们可以在需要时使用 手动定义类型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
Enter fullscreen mode Exit fullscreen mode

值得注意的是,setState与类组件相反,使用更新挂钩函数需要返回完整的状态。

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!
Enter fullscreen mode Exit fullscreen mode

另一件值得注意的有趣的事情是,我们可以通过将函数传递给来延迟启动状态useState

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

// const state: number
Enter fullscreen mode Exit fullscreen mode

再次,TypeScript 可以推断状态类型。

这意味着我们在处理时不需要做太多工作useState,只有在我们没有初始状态的情况下才需要做,因为实际状态形状可能会在最初渲染时计算出来。

useEffect

另一个基本钩子是useEffect,它在处理副作用(例如日志记录、突变或订阅事件监听器)时非常有用。通常,useEffect需要一个运行效果的函数,该效果可以选择返回一个清理函数,这对于取消订阅和删除监听器非常有用。此外,useEffect还可以提供第二个参数,其中包含一个值数组,以确保效果函数仅在其中一个值发生更改时运行。这确保我们可以控制效果的运行时间。

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

从文档中的原始示例来看,我们可以注意到,使用时不需要任何额外的输入useEffect
当我们尝试在 effect 函数中返回非函数或未定义的内容时,TypeScript 会报错。

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

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

useContext

useContext需要一个上下文对象并返回所提供上下文的值。当提供程序更新上下文时,会触发重新渲染。以下示例应该能够解释这一点:

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

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

再次强调,我们不需要对类型做太多处理。类型是可以推断出来的。

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
Enter fullscreen mode Exit fullscreen mode

useReducer

有时我们处理更复杂的状态,这些状态可能也依赖于先前的状态。useReducer接受一个函数,该函数根据先前的状态和动作计算特定状态。 以下示例取自官方文档

const [state, dispatch] = useReducer(reducer, initialArg, init);
Enter fullscreen mode Exit fullscreen mode

如果我们查看文档中的示例,我们会注意到需要做一些额外的输入工作。请查看略作修改的示例:

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

目前state无法正确推断。但我们可以通过为 Reducer 函数添加类型来改变这种情况。通过在Reducer 函数内部定义state和,我们现在可以推断所提供的。让我们调整一下这个例子。actionstateuseReducer

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

现在我们可以确保类型在内部推断Counter

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

当尝试分派不存在的类型时,我们将遇到错误。

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

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

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

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

从上面的例子中可以看出,useReducer由于函数reducer类型正确,类型是通过延迟初始化来推断的。

关于,我们不需要知道太多useReducer

使用回调

有时我们需要记忆回调。useCallback接受一个内联回调和一个输入数组,以便仅当其中一个值发生变化时才更新记忆。我们来看一个例子:

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

有趣的是,我们可以使用任何类型调用 memoizedCallback 并且不会看到 TypeScript 抱怨:

memoizedCallback("ok!"); // Works!
memoizedCallback(1); // Works!
Enter fullscreen mode Exit fullscreen mode

在这个特定情况下,memoizedCallback尽管函数需要两个数字,但它可以处理字符串或数字add。为了解决这个问题,我们需要在编写内联函数时更加具体。

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

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

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

使用备忘录

useMemo与 非常相似useCallback,但返回的是记忆值,而不是记忆回调。以下内容摘自文档。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Enter fullscreen mode Exit fullscreen mode

因此,如果我们根据上述内容构建一个示例,我们会注意到我们不需要对类型做任何事情:


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

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

useRef

最后,我们再看一个钩子:useRef
使用 时useRef,我们可以访问一个可变的引用对象。此外,我们可以将一个初始值传递给useRef,该初始值用于初始化current可变引用对象公开的属性。这在尝试访问函数 fe 内部的某些组件时很有用。再次,让我们使用文档中的示例。

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

我们可以看到 TypeScript 报错,因为我们useRef用初始化了null,这是一个有效的例子,因为有时设置引用可能会在稍后的时间点发生。
这意味着,我们在使用 时需要更加明确useRef

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

useRef使用via 定义实际类型时,更具体useRef<HTMLInputElement>仍然无法消除错误。实际上current,检查属性是否存在可以避免编译器报错。

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

useRef也可以用作实例变量
如果我们需要更新current属性,则需要使用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);
      }
    };
  });
  // ...
}
Enter fullscreen mode Exit fullscreen mode

关于 React Hooks,还有一些更有趣的内容需要学习,但这些内容并非 TypeScript 独有。如果您对此主题更感兴趣,请参阅React 官方文档中关于 Hooks 的内容
现在,我们应该对如何定义 React Hooks 的类型有了很好的理解。

如果您有任何问题或反馈,请在此处发表评论或通过 Twitter 联系:A. Sharif

文章来源:https://dev.to/busypeoples/notes-on-typescript-react-hooks-28j2
PREV
🧑‍💻🔧 使用这些应用搭建家庭实验室,开发任何东西!🤩
NEXT
Top 5 Data Visualization Libraries You Should Know in 2025