关于TypeScript的注释:React Hooks

发布于:2021-02-05 13:55:20

0

294

0

React TypeScript Hooks React Hooks

导言

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

React Hooks

在“TypeScript注释”系列的这一部分中,我们将了解如何使用TypeScript键入React Hooks,并在此过程中了解有关hooks的更多信息。

我们将参考官方React文档中关于hooks的内容,在需要了解更多关于hooks的信息或需要特定问题的具体答案时,这是一个非常有价值的来源。

通常,在16.8中添加了hooks来React,使开发人员能够在函数组件中使用状态,这在类组件中是唯一可能的。文档说明有基本挂钩和附加挂钩。

基本挂钩有useStateuseEffectuseContext,附加挂钩包括useReduceruseCallbackuseMemouseRef

useState

让我们从useState开始,这是一个基本的hooks,顾名思义,应该用于状态处理。

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

看看上面的例子,我们可以看到useState返回一个状态值以及一个函数来更新它。但是我们如何输入statesetState
有趣的是,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(null);
// const state: number | null
const [state, setState] = useState(null);
// const state: {id: number; name: string;} | null
const [state, setState] = useState(undefined);
// const state: number | null

可能还需要注意的是,与setState类内组件相反,使用update 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

另一个基本的hooks是useEffect,它在处理诸如日志记录、突变或订阅事件侦听器之类的副作用时非常有用。一般来说,useEffect需要一个函数来运行一个效果,该效果可以选择性地返回一个clean-up函数,该函数对于取消订阅和删除侦听器非常有用。此外,useEffect还可以提供第二个参数,该参数包含一个值数组,以确保仅当其中一个值被删除时,效果函数才会运行改变。这确保了我们可以控制何时运行效果。

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

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

TypeScript在尝试返回非函数或effect函数中未定义的内容时会抱怨。

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

同样,我们不需要做太多关于类型的事情。类型是推断出来的。

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}dispatch({type: 'increment'})}>+dispatch({type: 'decrement'})}>-</>
 );
}

目前无法正确推断。但是我们可以通过为reducer函数添加类型来改变这一点。通过在reducer函数中定义stateaction,我们现在可以推断useReducer提供的state。让我们改编一下这个例子。

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
 // ...
}

从上面的示例中可以看出,由于正确键入了reducer函数,因此使用延迟初始化的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

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

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

最后,我们再看一个hooks:useRef

当使用useRef时,我们可以访问可变的引用对象。此外,我们可以将初始值传递给useRef,该值用于初始化可变ref对象公开的current属性。这在尝试访问函数f.e.中的某些组件时非常有用。同样,让我们使用文档中的示例。

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 (<>Focus the input</>
 );
}

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

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

当通过定义实际类型来使用useRef时,更加具体化仍然不能消除错误。实际检查current属性是否存在,可以防止编译器抱怨。

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

useRef也可用作实例变量。

如果需要更新current属性,则需要将useRef与泛型类型Type | null一起使用:

function sleep() {
 const timeoutRefId = useRef();

 useEffect(() => {
   const id = setTimeout(() => {
     // ...
   });
   if (timeoutRefId.current) {
     timeoutRefId.current = id;
   }
   return () => {
     if (timeoutRefId.current) {
       clearTimeout(timeoutRefId.current);
     }
   };
 });
 // ...
}

关于React hooks,还有一些更有趣的东西需要学习,但不是特定于TypeScript的。