下载工作台
前端框架精通

TypeScript + React 18 实战

试读上半部分 · 解锁后可读全文

第 10 章 · TypeScript + React 18 实战

本章目标:掌握 React.FC vs 直接函数props interface 写法;为 useState 泛型、事件类型 建立习惯;配置 React Router自定义 Hook 类型;按步骤将 academy-demo-react 从 JS 迁移到 TS

学时建议:4~5 小时(含 2 小时迁移跟练)

前置:本模块 ch04~ch05、ch08;academy-demo-react 已能 npm run build


10.1 项目 TS 配置

cd academy-demo-react
npm install -D typescript @types/react @types/react-dom

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "skipLibCheck": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] }
  },
  "include": ["src"]
}

package.json

{
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "typecheck": "tsc --noEmit"
  }
}

src/vite-env.d.ts

/// <reference types="vite/client" />

vite.config.ts 与 ch09 相同结构,插件换 @vitejs/plugin-react

行业案例 · 贤紫优选商城:数据运营大屏 React 子应用从 CRA 迁至 Vite + TS;共享 @shared/types 包与 Vue 后台同源 OpenAPI。Hooks 单测覆盖 useDebounceuseLocalStorage,类型回归在 PR 阶段拦截 props 变更。

10.2 React.FC vs 直接函数组件

10.2.1 现代推荐:直接函数 + Props 类型

import type { Course } from '@/types/course'

type CourseCardProps = {
  course: Course
  compact?: boolean
  onSelect?: (id: string) => void
  onFavorite?: (id: string, value: boolean) => void
}

export function CourseCard({
  course,
  compact = false,
  onSelect,
  onFavorite
}: CourseCardProps) {
  return (
    <article
      className={compact ? 'course-card compact' : 'course-card'}
      onClick={() => onSelect?.(course.id)}
    >
      <h3>{course.title}</h3>
      <button
        type="button"
        onClick={e => {
          e.stopPropagation()
          onFavorite?.(course.id, true)
        }}
      >
        收藏
      </button>
    </article>
  )
}

10.2.2 React.FC(了解即可)

import type { FC } from 'react'

export const CourseCard: FC<CourseCardProps> = ({ course, compact = false }) => {
  return <article className="course-card">{course.title}</article>
}
对比直接函数React.FC
children显式写在 Props历史版本隐式注入
泛型组件自然支持较别扭
社区趋势React 19 文档推荐逐渐减少
本模块默认采用仅老代码保留

结论:用 function Component(props: Props) 或箭头函数 + 类型注解;不必强行 FC


10.3 Props:interface vs type

// types/course.ts — 与 ch08 共享
export interface Course {
  id: string
  title: string
  desc: string
  hours: number
  tags: readonly string[]
  level: 'beginner' | 'intermediate' | 'advanced'
  cover?: string
}

// 扩展 Props
export interface CourseListProps {
  courses: Course[]
  keyword: string
  onKeywordChange: (value: string) => void
  favorites: ReadonlySet<string>
  onToggleFavorite: (id: string) => void
}

组件文件:

import type { CourseListProps } from '@/types/course'

export function CourseList({
  courses,
  keyword,
  onKeywordChange,
  favorites,
  onToggleFavorite
}: CourseListProps) {
  return (
  <>
    <input
      value={keyword}
      onChange={e => onKeywordChange(e.target.value)}
      placeholder="搜索课程"
      aria-label="搜索课程"
    />
    {courses.map(c => (
      <CourseCard
        key={c.id}
        course={c}
        onFavorite={id => onToggleFavorite(id)}
      />
    ))}
  </>
  )
}

ReadonlySet / readonly 数组表达「父组件传入不可变」契约。


10.4 useState 泛型与事件类型

10.4.1 useState

const [keyword, setKeyword] = useState<string>('')
const [courses, setCourses] = useState<Course[]>([])
const [favorites, setFavorites] = useState<Set<string>>(() => new Set())
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle')

// 可空
const [detail, setDetail] = useState<CourseDetail | null>(null)

初始值为 null 时必须 useState<CourseDetail | null>(null),否则推断为 null 无法赋值。

10.4.2 事件类型

function SearchBar({ onChange }: { onChange: (v: string) => void }) {
  const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    onChange(e.target.value)
  }

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
  }

  const handleKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') e.currentTarget.blur()
  }

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleInput} onKeyDown={handleKey} />
    </form>
  )
}
事件类型
input changeChangeEvent<HTMLInputElement>
button clickMouseEvent<HTMLButtonElement>
form submitFormEvent<HTMLFormElement>
ul scrollUIEvent<HTMLUListElement>

技巧:用 e.currentTarget 而非 e.target,类型更精确。

10.4.3 useRef

const inputRef = useRef<HTMLInputElement>(null)

function focus() {
  inputRef.current?.focus()
}

// 保存可变值、不触发重渲染
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

10.5 自定义 Hook 类型

// hooks/useDebounce.ts
import { useEffect, useState } from 'react'

export function useDebounce<T>(value: T, delayMs: number): T {
  const [debounced, setDebounced] = useState(value)

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delayMs)
    return () => clearTimeout(id)
  }, [value, delayMs])

  return debounced
}
// hooks/useLocalStorage.ts
import { useCallback, useState } from 'react'

export function useLocalStorage<T>(
  key: string,
  initial: T
): [T, (value: T | ((prev: T) => T)) => void] {
  const [stored, setStored] = useState<T>(() => {
    const raw = localStorage.getItem(key)
    if (raw === null) return initial
    return JSON.parse(raw) as T
  })

  const setValue = useCallback(
    (value: T | ((prev: T) => T)) => {
      setStored(prev => {
        const next = typeof value === 'function'
          ? (value as (prev: T) => T)(prev)
          : value
        localStorage.setItem(key, JSON.stringify(next))
        return next
      })
    },
    [key]
  )

  return [stored, setValue]
}
// hooks/useCourses.ts
import { useEffect, useState } from 'react'
import type { Course } from '@/types/course'
import { fetchJson } from '@/utils'

export interface UseCoursesResult {
  courses: Course[]

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。