关于 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 包括useState
、useEffect
,useContext
附加 Hook 包括useReducer
、useCallback
、useMemo
、useRef
。
useState
让我们从基本的钩子开始useState
,顾名思义,它应该用于状态处理。
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
与类组件相反,使用更新挂钩函数需要返回完整的状态。
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(
() => {
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>
</>
);
}
目前state
无法正确推断。但我们可以通过为 Reducer 函数添加类型来改变这种情况。通过在Reducer 函数内部定义state
和,我们现在可以推断所提供的。让我们调整一下这个例子。action
state
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
由于函数reducer
类型正确,类型是通过延迟初始化来推断的。
关于,我们不需要知道太多useReducer
。
使用回调
有时我们需要记忆回调。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
与 非常相似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
最后,我们再看一个钩子: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>
</>
);
}
我们可以看到 TypeScript 报错,因为我们useRef
用初始化了null
,这是一个有效的例子,因为有时设置引用可能会在稍后的时间点发生。
这意味着,我们在使用 时需要更加明确useRef
。
function TextInputWithFocusButton() {
const inputEl = useRef<HTMLInputElement>(null);
const onButtonClick = () => {
inputEl.current.focus(); // Error! Object is possibly 'null'
};
// ...
}
useRef
使用via 定义实际类型时,更具体useRef<HTMLInputElement>
仍然无法消除错误。实际上current
,检查属性是否存在可以避免编译器报错。
function TextInputWithFocusButton() {
const inputEl = useRef<HTMLInputElement>(null);
const onButtonClick = () => {
if (inputEl.current) {
inputEl.current.focus(); // Works!
}
};
// ...
}
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);
}
};
});
// ...
}
关于 React Hooks,还有一些更有趣的内容需要学习,但这些内容并非 TypeScript 独有。如果您对此主题更感兴趣,请参阅React 官方文档中关于 Hooks 的内容。
现在,我们应该对如何定义 React Hooks 的类型有了很好的理解。
如果您有任何问题或反馈,请在此处发表评论或通过 Twitter 联系:A. Sharif
文章来源:https://dev.to/busypeoples/notes-on-typescript-react-hooks-28j2