- 译:101个React技巧#1组件组织
- 译:101个React技巧#2有效的设计模式与技术
- 译:101个React技巧#3Keys&Refs
- 译:101个React技巧#4组织React代码
- 译:101个React技巧#5高效状态管理
- 译:101个React技巧#6React代码优化
- 译:101个React技巧#7React代码调试技巧
- 译:101个React技巧#8测试 React代码
- 译:101个React技巧#9React hook
- 译:101个React技巧#10必知的React库/工具
- 译:101个React技巧#11React与Visual Studio Cod
- 译:101个React技巧#12React 与 TypeScript
- 译:101个React技巧#13其他技巧
82. 使用 ReactNode 替代 JSX.Element | null | undefined | ... 保持代码简洁
我经常看到这个错误。
与其这样定义 leftElement 和 rightElement 属性:
const Panel = ({
leftElement,
rightElement,
}: {
leftElement: JSX.Element | null | undefined;
// | ...;
rightElement: JSX.Element | null | undefined;
// | ...
}) => {
// …
};
你可以使用 ReactNode 让代码更简洁:
const MyComponent = ({
leftElement,
rightElement,
}: {
leftElement: ReactNode;
rightElement: ReactNode;
}) => {
// …
};
83. 使用 PropsWithChildren 简化需要 children 属性的组件类型
你不需要手动定义 children 属性类型。
实际上,你可以使用 PropsWithChildren 来简化类型定义。
// 🟠 可以
const HeaderPage = ({
children,
...pageProps
}: { children: ReactNode } & PageProps) => {
// …
};
// ✅ 更好
const HeaderPage = ({
children,
...pageProps
}: PropsWithChildren<PageProps>) => {
// …
};
84. 使用 ComponentProps、ComponentPropsWithoutRef 等高效访问元素属性
有些情况下你需要获取组件的属性。
例如,假设你想要一个点击时会打印日志的按钮。
你可以使用 ComponentProps 获取 button 元素的属性,然后重写 click 属性。
const ButtonWithLogging = (props: ComponentProps<"button">) => {
const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => {
console.log("按钮被点击"); //TODO: 更好的日志
props.onClick?.(e);
};
return <button {...props} onClick={handleClick} />;
};
这个技巧也适用于自定义组件。
const MyComponent = (props: { name: string }) => {
// …
};
const MyComponentWithLogging = (props: ComponentProps<typeof MyComponent>) => {
// …
};
85. 使用 MouseEventHandler、FocusEventHandler 等类型简化事件处理器类型
与其手动定义事件处理器类型,你可以使用 MouseEventHandler 等类型让代码更简洁易读。
// 🟠 可以
const MyComponent = ({
onClick,
onFocus,
onChange,
}: {
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
onFocus: (e: FocusEvent<HTMLButtonElement>) => void;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}) => {
// …
};
// ✅ 更好
const MyComponent = ({
onClick,
onFocus,
onChange,
}: {
onClick: MouseEventHandler<HTMLButtonElement>;
onFocus: FocusEventHandler<HTMLButtonElement>;
onChange: ChangeEventHandler<HTMLInputElement>;
}) => {
// …
};
86. 当类型无法或不应该从初始值推断时,在 useState、useRef 等中显式指定类型
当类型无法从初始值推断时,不要忘记显式指定类型。
例如,在下面的例子中,状态中存储了一个 selectedItemId,它应该是 string 或 undefined。
由于没有指定类型,TypeScript 会推断类型为 undefined,这不是我们想要的。
// ❌ 不好: `selectedItemId` 会被推断为 `undefined`
const [selectedItemId, setSelectedItemId] = useState(undefined);
// ✅ 好
const [selectedItemId, setSelectedItemId] = useState<string | undefined>(
undefined
);
💡 注意:相反的情况是当 TypeScript 能为你推断类型时,你不需要指定类型。
87. 使用 Record 类型编写更简洁、可扩展的代码
我喜欢这个辅助类型。
假设我有一个表示日志级别的类型。
type LogLevel = "info" | "warn" | "error";
我们为每个日志级别都有一个对应的函数来记录消息。
const logFunctions = {
info: (message: string) => console.info(message),
warn: (message: string) => console.warn(message),
error: (message: string) => console.error(message),
};
与其手动定义 logFunctions 的类型,你可以使用 Record 类型。
const logFunctions: Record<LogLevel, (message: string) => void> = {
info: (message: string) => console.info(message),
warn: (message: string) => console.warn(message),
error: (message: string) => console.error(message),
};
使用 Record 类型使代码更简洁易读。
此外,它还能在新日志级别添加或删除时捕获错误。
例如,如果我决定添加一个 debug 日志级别,TypeScript 会抛出错误。
88. 使用 as const 技巧准确类型化你的 hook 返回值
假设我们有一个 useIsHovered hook 来检测 div 元素是否被悬停。
这个 hook 返回一个用于 div 元素的 ref 和一个表示 div 是否被悬停的布尔值。
const useIsHovered = () => {
const ref = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
// TODO: 其余实现
return [ref, isHovered];
};
目前,TypeScript 不会正确推断函数返回类型。
你可以这样显式定义返回类型来修复:
const useIsHovered = (): [RefObject<HTMLDivElement>, boolean] => {
// TODO: 其余实现
return [ref, isHovered];
};
或者你可以使用 as const 技巧准确类型化返回值:
const useIsHovered = () => {
// TODO: 其余实现
return [ref, isHovered] as const;
};
89. Redux: 参考 react-redux.js.org/using-react… 确保正确类型化你的 Redux 状态和辅助函数
我喜欢使用 Redux 来管理复杂的客户端状态。
它也能很好地与 TypeScript 配合使用。
你可以在这里找到一个关于如何在 TypeScript 中使用 Redux 的很好的指南 这里。
90. 使用 ComponentType 简化你的类型
假设你正在设计一个类似 Figma 的应用(我知道,你很有野心 😅)。
这个应用由多个小部件组成,每个小部件都接受一个 size。
为了重用逻辑,我们可以定义一个共享的 WidgetWrapper 组件,它接受一个 Widget 类型的部件,定义如下:
interface Size {
width: number;
height: number;
}
interface Widget {
title: string;
Component: ComponentType<{ size: Size }>;
}
WidgetWrapper 组件会渲染小部件并传递相应的大小给它。
const WidgetWrapper = ({ widget }: { widget: Widget }) => {
const { Component, title } = widget;
const { onClose, size, onResize } = useGetProps(); //TODO: 更好的名字但你懂的 😅
return (
<Wrapper onClose={onClose} onResize={onResize}>
<Title>{title}</Title>
{/* 我们可以用 size 渲染下面的组件 */}
<Component size={size} />
</Wrapper>
);
};
91. 使用 TypeScript 泛型让你的代码更具可重用性
如果你没有使用 TypeScript 泛型,只有两种可能:
- 你要么在写非常简单的代码,要么
- 你错过了好东西 😅
TypeScript 泛型让你的代码更具可重用性和灵活性。
例如,假设我在博客上有不同的项目(如 Post、Follower 等),我想要一个通用的列表组件来显示它们。
export interface Post {
id: string;
title: string;
contents: string;
publicationDate: Date;
}
export interface User {
username: string;
}
export interface Follower extends User {
followingDate: Date;
}
每个列表都应该是可排序的。
有不好和好的两种方式来做这件事。
❌ 不好: 我创建一个接受项目联合类型的单一列表组件。
这不好因为:
- 每次添加新项目时,都必须更新函数/类型
- 函数不是完全类型安全的(见
这不应该发生的注释) - 这段代码依赖于其他文件(如
FollowerItem、PostItem) - 等等
import { FollowerItem } from "./FollowerItem";
import { PostItem } from "./PostItem";
import { Follower, Post } from "./types";
type ListItem =
| { type: "follower"; follower: Follower }
| { type: "post"; post: Post };
function ListBad({
items,
title,
vertical = true,
ascending = true,
}: {
title: string;
items: ListItem[];
vertical?: boolean;
ascending?: boolean;
}) {
const sortedItems = [...items].sort((a, b) => {
const sign = ascending ? 1 : -1;
return sign * compareItems(a, b);
});
return (
<div>
<h3 className="title">{title}</h3>
<div className={`list ${vertical ? "vertical" : ""}`}>
{sortedItems.map((item) => (
<div key={getItemKey(item)}>{renderItem(item)}</div>
))}
</div>
</div>
);
}
function compareItems(a: ListItem, b: ListItem) {
if (a.type === "follower" && b.type === "follower") {
return (
a.follower.followingDate.getTime() - b.follower.followingDate.getTime()
);
} else if (a.type == "post" && b.type === "post") {
return a.post.publicationDate.getTime() - b.post.publicationDate.getTime();
} else {
// 这不应该发生
return 0;
}
}
function getItemKey(item: ListItem) {
switch (item.type) {
case "follower":
return item.follower.username;
case "post":
return item.post.id;
}
}
function renderItem(item: ListItem) {
switch (item.type) {
case "follower":
return <FollowerItem follower={item.follower} />;
case "post":
return <PostItem post={item.post} />;
}
}
相反,我们可以使用 TypeScript 泛型创建一个更具可重用性和类型安全的列表组件。
我在下面的沙盒中做了一个例子 👇。
92. 使用 NoInfer 实用类型确保精确的类型
想象你正在开发一个视频游戏 🎮。
游戏有多个地点(如 LeynTir、Forin、Karin 等)。
你想创建一个将玩家传送到新地点的函数。
function teleportPlayer<L extends string>(
position: Position,
locations: L[],
defaultLocation: L
): L {
// 传送玩家并返回地点
}
这个函数会这样调用:
const position = { x: 1, y: 2, z: 3 };
teleportPlayer(position, ["LeynTir", "Forin", "Karin"], "Forin");
teleportPlayer(position, ["LeynTir", "Karin"], "anythingCanGoHere"); // ❌ 这会工作,但它是错误的,因为 "anythingCanGoHere" 不应该是有效地点
第二个例子是无效的,因为 anythingCanGoHere 不是有效地点。
然而,TypeScript 不会抛出错误,因为它从列表和默认地点推断出了 L 的类型。
要修复这个问题,使用 NoInfer 实用类型。
function teleportPlayer<L extends string>(
position: Position,
locations: L[],
defaultLocation: NoInfer<L>
): NoInfer<L> {
// 传送玩家并返回地点
}
现在 TypeScript 会抛出错误:
teleportPlayer(position, ["LeynTir", "Karin"], "anythingCanGoHere"); // ❌ 错误: 类型 '"anythingCanGoHere"' 不能赋值给参数类型 '"LeynTir" | "Karin"'
使用 NoInfer 实用类型确保默认地点必须是列表中提供的有效地点之一,防止无效输入。
93. 使用 ElementRef 类型辅助轻松类型化 refs
有难和易两种方式类型化 refs。
难的方式是记住元素的类型名并直接使用它 🤣。
const ref = useRef<HTMLDivElement>(null);
简单的方式是使用 ElementRef 类型辅助。这个方法更直接,因为你应该已经知道元素的名称。
const ref = useRef<ElementRef<"div">>(null);