6. React
6.1. 概述
目前,TypeScript
已经在 React
上得到了良好的支持。项目上目前采用 React 16
版本用做开发。此规范会定义一些我们日常使用
React API 所需注入的类型。
6.2. 组件类型
6.2.1. 函数组件
必须函数组件采用 React.FC
类型进行类型定义。
interface Props {
message: string;
}
const App: React.FC<Props> = ({message, children}) => (
<div>{message}{children}</div>
);
React.FC
不仅校验组件参数类型和返回的 ReactElement
,他同时又校验了 displayName
、propTypes
、defaultProps
、contextTypes
属性类型。
6.2.2. 路由组件
必须不同项目安装了不同的 react-router
版本,主要有 v4 和 v5 两大版本。针对不同版本推荐采用不同的方式进行路由参数的使用。
react-router v4
非顶层组件需要通过高阶组件的方式,使用 withRouter
包装组件来获取路由参数。
那么顶层组件或路由包装组件的类型定义就应当从 react-router
上的 RouteComponentProps
继承。
import {RouteComponentProps} from 'react-router';
interface Props extends RouteComponentProps {
message: string;
}
const App: React.FC<Props> = ({message, children, history}) => (
<div>{message}{children}</div>
);
如果路由组件接收路由参数,可以通过 RouteComponentProps
提供的泛型参数注入路由参数类型。RouteComponentProps
允许接收3个泛型参数。
- 第一个参数是路由
params
参数的类型。 - 第二个参数是路由
statusCode
参数,表示当前路由状态编码是 404、500 还是其他。 - 第三个参数是路由
state
参数的类型。
import {RouteComponentProps, StaticContext, withRouter} from 'react-router';
interface Props extends RouteComponentProps<{
id: string;
type: string;
},
StaticContext,
{
readonly: boolean;
}> {
message: string;
}
const App: React.FC<Props> = ({
history,
match: {params: {id, type}},
location: {state: routeState},
message,
children,
}) => (
<div>{message}{children}</div>
);
export default withRouter(App);
react-router v5(推荐)
任意组件可以采用 react-route
提供的 hooks 获取路由参数信息,主要有以下几个 hooks:
- useHistory: 访问 history 对象,进行编程式导航(如 push 或 replace 路由)。
- useLocation: 返回当前的 location 对象,表示应用的当前位置,通常用于获取当前 URL、路径名、状态等信息。
- useParams: 获取当前路由的 URL 参数,常用于从 URL 中提取动态值。
- useRouteMatch: 匹配当前 URL 与某个特定路径,类似于 v5 中的 match 对象,可以用于自定义路径匹配。
import {useHistory, useLocation, useParams, useRouteMatch} from 'react-router';
interface Props {
}
const App: React.FC<Props> = () => {
const history = useHistory();
const {id: scriptId, type: viewType} = useParams<{ id: string; type: string }>();
const {state: routeState = {readonly: false}} = useLocation<{ readonly: boolean }>();
const match = useRouteMatch<{ id: string; type: string }>();
function handleClick() {
history.push("/home");
}
return (
<button type="button" onClick={handleClick}>
Go home
</button>
);
};
因为 useLocation
没有暴露出 query
的参数类型,因此在 apaas 包中定义了一个 useSearchParams
的 hook,用于获取 query 参数。
import useSearchParams from 'hzero-front-apaas/lib/hooks/useSearchParams';
interface Props {
}
const App: React.FC<Props> = () => {
const {id} = useSearchParams<{id: string}>();
return id
};
使用 react-route 提供的 hooks 时,对于需要泛型的 hook 尽量写明类型
6.3. Hooks 类型
6.3.1. useState
可选useState 接收一个泛型指定其传入的数据类型,如果不传入则 TypeScript
根据初始值进行类型推断。
const [user, setUser] = useState<string>('张三');
如果要给 useState
初始值设置一个空值,可以把空值添加到泛型中,或者在 useState
函数中直接设置初始值,禁止使用 as
覆盖初始值类型。
const [user, setUser] = useState<User | null>(null);
6.3.2. useEffect/useLayoutEffect
必须useEffect
和 useLayoutEffect
都用于执行副作用,并返回一个可选的清理函数
,这意味着如果它们不处理返回值,就不需要类型。当使用 useEffect
时,注意不要返回非 function
或 undefined
的内容。
// 不要这样做
const DelayedEffect: React.FC<{ timerMs: number }> = ({timerMs}) => {
const {timerMs} = props;
useEffect(
() =>
setTimeout(() => {
/* do stuff */
}, timerMs),
[timerMs]
);
return null;
}
// 应当这样做
const DelayedEffect: React.FC<{ timerMs: number }> = ({timerMs}) => {
const {timerMs} = props;
useEffect(() => {
setTimeout(() => {
/* do stuff */
}, timerMs);
}, [timerMs]);
return null;
}
6.3.3. useMemo/useCallback
可选useMemo
和 useCallback
都可选接收一个泛型,用于指定返回值类型,如果没有指定则通过类型推断。
const App: React.FC<{}> = () => {
const randomNum = useMemo<number>(() => {
return Math.random();
}, [])
return <div>{randomNum}</div>
};
6.3.4. useRef
应当useRef
返回了一个引用,该引用类型可以是只读
或可修改
,应在 useRef
显式指定泛型,尽量减少使用 any
。
const numberRef = useRef<number>(0);
如果需要 useRef
的类型可修改
,就需要在泛型参数中包含 | null
如果 useRef
绑定的是 DOM
元素,那么就需要提供元素类型作为参数,并使用 null
作为初始值。
interface Props {
}
const App: React.FC<Props> = ({children}) => {
const divRef = useRef<HTMLDivElement>(null);
return <div ref={divRef}>{children}</div>
};
对于已知元素的标签,必须使用对应标签的 HTMLElement 类型,例如 div
对应 HTMLDivElement
, input
对应 HTMLInputElement
。不应当使用 HTMLElement
。
6.3.5. useImperativeHandle
应当useImperativeHandle
应当搭配 forwardRef
进行使用。forwardRef
需通过泛型定义分别指定 ref
和 组件 props
类型。
interface RefProps {
getName: () => string;
}
interface Props {
message: string;
}
const App = forwardRef<RefProps, Props>(({message, children}, ref) => {
useImperativeHandle(ref, () => ({
getName() {
return 'hello';
},
}));
return <div>{message}{children}</div>
});
const Use: React.FC<{}> = () => {
const appRef = useRef<RefProps>(null)
return <App message="hello" ref={appRef} />
}
6.3.6. useContext
应当useContext
需要搭配 createContext
进行使用。createContext
在创建的时候允许接收一个泛型用于指定返回的上下文类型。
import {createContext} from "react";
interface AppContextInterface {
name: string;
author: string;
url: string;
}
const AppCtx = createContext<AppContextInterface | null>(null);
// Provider in your app
const sampleAppContext: AppContextInterface = {
name: "Using React Context in a Typescript App",
author: "thehappybug",
url: "http://www.example.com",
};
export const App = () => (
<AppCtx.Provider value={sampleAppContext}>...</AppCtx.Provider>
);
// Consume in your app
import {useContext} from "react";
export const PostInfo: React.FC<{}> = () => {
const appContext = useContext(AppCtx);
return (
<div>
Name: {appContext.name}, Author: {appContext.author}, Url:{" "}
{appContext.url}
</div>
);
};
6.3.7. useSafeState
(ahooks)
useSafeState
主要用于在组件卸载后异步回调内的 setState
不再执行,避免因组件卸载后更新状态而导致的内存泄漏。
对于接口请求后需要异步更新状态的场景,推荐使用 useSafeState
。
import React, { useEffect } from 'react';
import { useSafeState } from 'ahooks';
const Child = () => {
const [value, setValue] = useSafeState<string>();
useEffect(() => {
setTimeout(() => {
setValue('data loaded from server');
}, 5000);
}, []);
const text = value || 'Loading...';
return <div>{text}</div>;
};
6.3.10. 自定义 hooks
如果在自定义 hooks
中返回一个数组,TypeScript
会推断出一个联合类型而不是元组类型,我们可以使用 as const
,把返回的数组指定成元组类型。
import {useState} from "react";
export function useLoading() {
const [isLoading, setState] = useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as const; // 使用 [boolean, typeof load] 代替 (boolean | typeof load)[]
}
6.4. 表单事件
应当针对表单类型,React 同样提供了丰富的类型。在使用 input 等这类表单元素时,需要指定例如 onChange
等事件类型( IDE
工具也会给出类型提示)。
interface Props {
}
const App: React.FC<Props> = ({children}) => {
const [text, setText] = useState<string>("");
const onChange = useCallback((e: React.FormEvent<HTMLInputElement>) => {
setText(e.currentTarget.value);
}, []);
return (
<div>
<input type="text" value={text} onChange={onChange} />
</div>
);
};
在 React 中,有一个很重要的概念就是:合成事件
。他是基于 Virtual DOM
所实现的一套事件系统。我们在 React Element
中所定义的事件,会作为合成事件来处理,其对应的事件处理函数,会接收到一个 SyntheticEvent
的实例。
合成事件有什么优势?
- 抹平各浏览器之间的事件差异,不存在兼容性问题,对开发者极为友好。
- 合成事件利用冒泡机制,在顶层 document 完成事件注册和分发,避免直接操作 DOM 事件,减少内存开销,简化事件处理和回收机制。
- 内部使用事件池的概念,管理合成事件的创建,回收及其复用,提升性能。
因此我们也可以用合成事件( SyntheticEvent
)作为通用的类型,去合并 form 的 onSubmit
事件类型。
<form
ref={formRef}
onSubmit={(e: React.SyntheticEvent) => {
e.preventDefault();
const target = e.target as typeof e.target & {
email: { value: string };
password: { value: string };
};
const email = target.email.value; // typechecks!
const password = target.password.value; // typechecks!
// etc...
}}
>
<div>
<label>
Email:
<input type="email" name="email" />
</label>
</div>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Log in" />
</div>
</form>
6.5. cloneElement
应当React.cloneElement
可以用来复制元素,并且可以添加新的 props
。React.cloneElement
允许传入一个泛型参数,用于指定 props
类型。
import {Button} from "choerodon-ui/pro";
// ...
const btn = <Button />;
React.cloneElement<typeof Button>(btn, {
color: 'primary',
})
6.6. 组件和 Hook 必须是幂等的
必须组件必须始终根据其输入(props、state、和 context)返回相同的输出。这被称为“幂等性
”。幂等性
是函数式编程中经常使用的一个术语,它指的是只要你使用相同的输入运行代码,得到的结果总是一样的。
这意味着,为了遵循这一规则,所有在渲染期间执行的代码也必须是幂等的。例如,以下这行代码就不是幂等的(因此,包含这行代码的组件也不是幂等的):
function Clock() {
const time = new Date(); // 🔴 错误的:总是返回不同的结果!
return <span>{time.toLocaleString()}</span>
}
可以把副作用从组件中抽离出来,放到一个单独的自定义 hook 中。例:
import { useState, useEffect } from 'react';
function useTime() {
// 1. 跟踪当前日期的状态。`useState` 接受一个初始化函数作为其
// 初始状态。它只在调用 Hook 时运行一次,因此只有调用 Hook 时的
// 当前日期才被首先设置。
const [time, setTime] = useState(() => new Date());
useEffect(() => {
// 2. 使用 `setInterval` 每秒更新当前日期。
const id = setInterval(() => {
setTime(new Date()); // ✅ 正确的:非幂等代码不再在渲染中运行。
}, 1000);
// 3. 返回一个清理函数,这样我们就不会忘记清理 `setInterval` 定时器,导致内存泄漏。
return () => clearInterval(id);
}, []);
return time;
}
export default function Clock() {
const time = useTime();
return <span>{time.toLocaleString()}</span>;
}
6.7. 副作用必须在渲染之外执行
副作用不应该在渲染中执行,因为 React 可能会多次渲染组件以提供最佳的用户体验。
6.7.1. 局部 mutation
必须禁止修改在组件外声明的变量。
const items = []; // 🔴 错误的:在组件外部创建
function FriendList({ friends }) {
for (let i = 0; i < friends.length; i++) {
const friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
); // 🔴 错误的:修改了一个在渲染之外创建的值。
}
return <section>{items}</section>;
}
可以在组件内部渲染前进行修改。
function FriendList({ friends }) {
const items = []; // ✅ 正确的:在局部创建
for (let i = 0; i < friends.length; i++) {
const friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
); // ✅ 正确的:局部修改是可以的。
}
return <section>{items}</section>;
}
6.7.2. 改变 DOM
必须在 React 组件的渲染逻辑中不允许有直接对用户可见的副作用。换句话说,仅仅调用一个组件函数本身不应当在屏幕上产生变化。
function ProductDetailPage({ product }) {
document.window.title = product.title; // 🔴 错误的:改变 DOM
}
可以使用 useEffect
/useLayoutEffect
处理渲染外副作用。
6.8. 参数不可变性
6.8.1. 不要修改 props
必须props 是不可变的,因为如果你改变了它们,应用程序可能会产生不一致的结果,这会让调试变得困难,因为程序可能会在某些情况下工作,而在另一些情况下不工作。
function Post({ item }) {
item.url = new Url(item.url, base); // 🔴 错误的:永远不要直接修改 props
return <Link url={item.url}>{item.title}</Link>;
}
function Post({ item }) {
const url = new Url(item.url, base); // ✅ 正确的:创建一个新的副本替代
return <Link url={url}>{item.title}</Link>;
}
6.8.2. 不要修改 state
必须我们不应该直接在 state 变量上进行更新,而应该使用 useState
返回的 setter 函数来进行更新。如果在 state 变量上直接修改值,并不会导致组件界面更新,这样用户界面就会显示过时的信息。
通过使用 setter 函数,我们告诉 React 状态已经发生了变化,需要进行重新渲染,以便更新用户界面。
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
count = count + 1; // 🔴 错误的:永远不要直接修改 state
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // ✅ 正确的:使用由 useState 返回的 setter 函数来修改 state。
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
6.8.3. 不要修改 Hook 的返回值和参数
必须一旦值被传递给 Hook,就不应该再对它们进行修改。就像在 JSX 中的 props 一样,当值被传递给 Hook 时,它们就应该是不可变的了。
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
if (icon.enabled) {
icon.className = computeStyle(icon, theme); // 🔴 错误的:永远不要直接修改 Hook 的参数。
}
return icon;
}
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
const newIcon = { ...icon }; // ✅ 正确的:创建一个新的副本替代
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}
6.8.4. 不要改变传递给 JSX 后的值
不要在 JSX 使用过值之后改变它们。应该在创建 JSX 之前完成值的更改。
当你在表达式中使用 JSX 时,React 可能会在组件完成渲染之前就急于计算 JSX。这意味着,如果在将值传递给 JSX 之后对它们进行更改,可能会导致 UI 过时,因为 React 不会知道需要更新组件的输出。
function Page({ colour }) {
const styles = { colour, size: "large" };
const header = <Header styles={styles} />;
styles.size = "small"; // 🔴 错误的:styles 已经在上面的 JSX 中使用了。
const footer = <Footer styles={styles} />;
return (
<>
{header}
<Content />
{footer}
</>
);
}
function Page({ colour }) {
const headerStyles = { colour, size: "large" };
const header = <Header styles={headerStyles} />;
const footerStyles = { colour, size: "small" }; // ✅ 正确的:我们创建了一个新的值。
const footer = <Footer styles={footerStyles} />;
return (
<>
{header}
<Content />
{footer}
</>
);
}
6.9. Hook 的规则
Hook 是使用 JavaScript 函数定义的,但它们代表了一种特殊的可重用的 UI 逻辑,并且对它们可以被调用的位置有限制。
6.9.1 只在顶层调用 Hook
必须不要在循环、条件语句、嵌套函数或 try/catch/finally
代码块中调用 Hook。相反,你应该在 React 函数组件的顶层使用 Hook,且在任何提前返回之前。你只能在 React 渲染函数组件时调用 Hook:
- ✅ 在 函数组件主体 的顶层调用它们。
- ✅ 在 自定义 Hook 主体 的顶层调用它们。
function Counter() {
// ✅ 正确的:在函数组件顶层
const [count, setCount] = useState(0);
// ...
}
function useWindowWidth() {
// ✅ 正确的:在自定义 Hooks 顶层
const [width, setWidth] = useState(window.innerWidth);
// ...
}
不支持在其他任何情况下调用以 use
开头的 Hook,例如:
- 🔴 不要在条件语句或循环中调用 Hook。
- 🔴 不要在条件性的
return
语句之后调用 Hook。 - 🔴 不要在事件处理函数中调用 Hook。
- 🔴 不要在类组件中调用 Hook。
- 🔴 不要在传递给
useMemo
、useReducer
或useEffect
的函数内部调用 Hook。 - 🔴 不要在
try/catch/finally
代码块中调用 Hook。
function Bad({ cond }) {
if (cond) {
// 🔴 错误的:在条件语句内部(要修复这个问题,将其移到外部!)
const theme = useContext(ThemeContext);
}
// ...
}
function Bad() {
for (let i = 0; i < 10; i++) {
// 🔴 错误的:在循环语句内部(要修复这个问题,将其移到外部!)
const theme = useContext(ThemeContext);
}
// ...
}
function Bad({ cond }) {
if (cond) {
return;
}
// 🔴 错误的:在条件性 return 语句之后(要修复这个问题,将其移到 return 之前!)
const theme = useContext(ThemeContext);
// ...
}
function Bad() {
function handleClick() {
// 🔴 错误的:在事件处理函数内部(要修复这个问题,将其移到 return 之前!)
const theme = useContext(ThemeContext);
}
// ...
}
function Bad() {
const style = useMemo(() => {
// 🔴 错误的:在 useMemo 内部调用(要修复这个问题,将其移到外部!)
const theme = useContext(ThemeContext);
return createStyle(theme);
});
// ...
}
class Bad extends React.Component {
render() {
// 🔴 错误的:在类组件内部调用(要修复这个问题,改写为函数组件!)
useEffect(() => {})
// ...
}
}
function Bad() {
try {
// 🔴 错误的:在 try、catch、finally 代码块内部调用(要修复这个问题,将其移到外部!)
const [x, setX] = useState(0);
} catch {
const [x, setX] = useState(1);
}
}
6.9.2 仅在 React 函数中调用 Hook
必须不要在常规的 JavaScript 函数中调用 Hook。相反,你可以:
- ✅ 在 React 函数组件中调用 Hook。
- ✅ 在 自定义 Hook 中调用 Hook。
遵循这条规则,你可以确保组件中的所有状态逻辑在其源代码中清晰可见。