TypeScript 开发指南 / 19 - React + TypeScript
React + TypeScript
项目初始化
# 使用 Vite 创建 React + TypeScript 项目
npm create vite@latest my-app -- --template react-ts
# 或使用 Create React App
npx create-react-app my-app --template typescript
组件类型
函数组件
// 方式一:使用 React.FC(不推荐,有隐式 children 问题)
const Greeting: React.FC<{ name: string }> = ({ name }) => {
return <h1>Hello, {name}!</h1>;
};
// 方式二:直接注解 props(推荐)
interface GreetingProps {
name: string;
}
function Greeting({ name }: GreetingProps) {
return <h1>Hello, {name}!</h1>;
}
// 方式三:箭头函数
const Greeting = ({ name }: GreetingProps) => {
return <h1>Hello, {name}!</h1>;
};
Props 类型
interface ButtonProps {
// 必选属性
children: React.ReactNode;
variant: "primary" | "secondary" | "danger";
// 可选属性
size?: "sm" | "md" | "lg";
disabled?: boolean;
// 事件处理
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
// HTML 属性透传
className?: string;
style?: React.CSSProperties;
id?: string;
// 渲染函数
icon?: React.ReactNode;
renderSuffix?: () => React.ReactNode;
}
function Button({
children,
variant,
size = "md",
disabled = false,
onClick,
className,
style,
icon,
renderSuffix
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size} ${className || ""}`}
style={style}
disabled={disabled}
onClick={onClick}
>
{icon && <span className="btn-icon">{icon}</span>}
{children}
{renderSuffix?.()}
</button>
);
}
// 使用
<Button variant="primary" onClick={() => console.log("clicked")}>
Click me
</Button>
children 类型
interface CardProps {
title: string;
// children 是 React.ReactNode
children: React.ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// 使用
<Card title="User Profile">
<p>Name: Alice</p>
<p>Age: 25</p>
</Card>
常用 React 类型
// React.ReactNode - 可渲染的内容
const node: React.ReactNode = "Hello";
const node2: React.ReactNode = <div>World</div>;
const node3: React.ReactNode = null;
const node4: React.ReactNode = undefined;
const node5: React.ReactNode = [1, 2, 3];
const node6: React.ReactNode = true; // 不渲染
// React.ReactElement - JSX 元素
const element: React.ReactElement = <div>Hello</div>;
// React.CSSProperties - CSS 样式对象
const style: React.CSSProperties = {
color: "red",
fontSize: 16,
backgroundColor: "#fff"
};
// React.HTMLAttributes - HTML 属性
interface Props extends React.HTMLAttributes<HTMLDivElement> {
custom: string;
}
State 类型
useState
// 类型自动推断
const [count, setCount] = useState(0); // number
const [name, setName] = useState("Alice"); // string
const [active, setActive] = useState(true); // boolean
// 初始值为 null
const [user, setUser] = useState<User | null>(null);
// 复杂对象
interface FormState {
username: string;
email: string;
errors: Record<string, string>;
}
const [form, setForm] = useState<FormState>({
username: "",
email: "",
errors: {}
});
// 更新对象
setForm(prev => ({
...prev,
username: "Alice"
}));
// 函数式初始化
const [data, setData] = useState<User[]>(() => {
return JSON.parse(localStorage.getItem("users") || "[]");
});
useReducer
// 定义状态类型
interface State {
count: number;
loading: boolean;
error: string | null;
}
// 定义 action 类型
type Action =
| { type: "INCREMENT" }
| { type: "DECREMENT" }
| { type: "RESET" }
| { type: "SET_LOADING"; payload: boolean }
| { type: "SET_ERROR"; payload: string | null };
// reducer 函数
function reducer(state: State, action: Action): State {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
case "RESET":
return { ...state, count: 0 };
case "SET_LOADING":
return { ...state, loading: action.payload };
case "SET_ERROR":
return { ...state, error: action.payload };
default:
return state;
}
}
// 使用
const [state, dispatch] = useReducer(reducer, {
count: 0,
loading: false,
error: null
});
dispatch({ type: "INCREMENT" });
dispatch({ type: "SET_ERROR", payload: "Something went wrong" });
Hooks 类型
useRef
// DOM 引用
const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current: HTMLInputElement | null
useEffect(() => {
inputRef.current?.focus();
}, []);
<input ref={inputRef} />
// 可变引用
const countRef = useRef<number>(0);
// countRef.current: number
// 回调 ref
const [height, setHeight] = useState(0);
const measuredRef = useCallback((node: HTMLDivElement | null) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
useEffect
// 基本用法
useEffect(() => {
// 副作用
const timer = setInterval(() => {
console.log("tick");
}, 1000);
// 清理函数
return () => {
clearInterval(timer);
};
}, []); // 依赖数组
// 依赖类型
useEffect(() => {
fetchData(userId);
}, [userId]); // userId 变化时重新执行
useMemo 和 useCallback
// useMemo - 缓存计算结果
const expensiveResult = useMemo(() => {
return items.filter(item => item.active).sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// useCallback - 缓存函数
const handleClick = useCallback((id: number) => {
setSelectedId(id);
}, [setSelectedId]);
// 带泛型的 useMemo
const filtered = useMemo<User[]>(() => {
return users.filter(u => u.active);
}, [users]);
自定义 Hook 类型
// 自定义 Hook 返回类型
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((prev: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue];
}
// 使用
const [name, setName] = useLocalStorage<string>("name", "");
const [settings, setSettings] = useLocalStorage<Settings>("settings", defaultSettings);
事件处理类型
// 表单事件
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
};
// 输入事件
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
// 键盘事件
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
submit();
}
};
// 鼠标事件
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX, e.clientY);
};
// 焦点事件
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
e.target.select();
};
泛型组件
// 泛型列表组件
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
emptyMessage?: string;
}
function List<T>({
items,
renderItem,
keyExtractor,
emptyMessage = "No items"
}: ListProps<T>) {
if (items.length === 0) {
return <div className="empty">{emptyMessage}</div>;
}
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// 使用
<List<User>
items={users}
keyExtractor={user => user.id}
renderItem={(user) => <span>{user.name}</span>}
/>
泛型 Select 组件
interface SelectOption<T> {
value: T;
label: string;
}
interface SelectProps<T> {
options: SelectOption<T>[];
value: T | null;
onChange: (value: T) => void;
placeholder?: string;
}
function Select<T extends string | number>({
options,
value,
onChange,
placeholder
}: SelectProps<T>) {
return (
<select
value={value ?? ""}
onChange={(e) => {
const selected = options.find(
opt => String(opt.value) === e.target.value
);
if (selected) onChange(selected.value);
}}
>
{placeholder && <option value="">{placeholder}</option>}
{options.map(opt => (
<option key={String(opt.value)} value={String(opt.value)}>
{opt.label}
</option>
))}
</select>
);
}
Context 类型
interface AuthContextType {
user: User | null;
loading: boolean;
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// 自定义 Hook 访问 Context
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within AuthProvider");
}
return context;
}
// Provider 组件
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const login = async (credentials: LoginCredentials) => {
const response = await api.post<User>("/auth/login", credentials);
setUser(response);
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
forwardRef 类型
interface InputProps {
label: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...props }, ref) => {
return (
<div className="form-field">
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span className="error">{error}</span>}
</div>
);
}
);
// 使用
const ref = useRef<HTMLInputElement>(null);
<Input ref={ref} label="Username" />
业务场景:表单组件
interface FormField<T> {
name: keyof T;
label: string;
type: "text" | "email" | "password" | "number" | "select";
required?: boolean;
options?: { value: string; label: string }[];
validate?: (value: T[keyof T]) => string | null;
}
interface DynamicFormProps<T extends Record<string, any>> {
fields: FormField<T>[];
initialValues: T;
onSubmit: (values: T) => Promise<void>;
}
function DynamicForm<T extends Record<string, any>>({
fields,
initialValues,
onSubmit
}: DynamicFormProps<T>) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [submitting, setSubmitting] = useState(false);
const handleChange = (name: keyof T, value: T[keyof T]) => {
setValues(prev => ({ ...prev, [name]: value }));
setErrors(prev => ({ ...prev, [name]: undefined }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// 验证
const newErrors: Partial<Record<keyof T, string>> = {};
for (const field of fields) {
if (field.required && !values[field.name]) {
newErrors[field.name] = `${field.label} 是必填项`;
}
if (field.validate) {
const error = field.validate(values[field.name]);
if (error) newErrors[field.name] = error;
}
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setSubmitting(true);
try {
await onSubmit(values);
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
{fields.map(field => (
<div key={String(field.name)}>
<label>{field.label}</label>
<input
type={field.type}
value={String(values[field.name] ?? "")}
onChange={e => handleChange(field.name, e.target.value as T[keyof T])}
/>
{errors[field.name] && (
<span className="error">{errors[field.name]}</span>
)}
</div>
))}
<button type="submit" disabled={submitting}>
{submitting ? "提交中..." : "提交"}
</button>
</form>
);
}
注意事项
- 避免使用
React.FC——它有隐式 children 类型(React 18 已修复) - 事件处理类型——根据事件源选择正确的事件类型
useRef 的两种用法——DOM 引用用 null 初始化,可变引用不用- 泛型组件——通过泛型参数使组件可复用
- Context 默认值——使用
undefined 配合自定义 Hook 抛出错误
扩展阅读