强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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

注意事项

  1. 避免使用 React.FC——它有隐式 children 类型(React 18 已修复)
  2. 事件处理类型——根据事件源选择正确的事件类型
  3. useRef 的两种用法——DOM 引用用 null 初始化,可变引用不用
  4. 泛型组件——通过泛型参数使组件可复用
  5. Context 默认值——使用 undefined 配合自定义 Hook 抛出错误

扩展阅读