直接跳到内容

React TSX

什么是 TSX?

TSX 是 TypeScript 与 JSX 的结合,它允许我们在 TypeScript 文件中编写类似 HTML 的语法来创建 React 组件。

TSX 的优势

  • 类型安全:编译时类型检查,减少运行时错误
  • 更好的开发体验:智能提示、自动补全和重构支持
  • 代码可维护性:明确的接口定义,便于团队协作

定义组件 Props 类型

使用接口定义 Props

tsx
interface ButtonProps {
  text: string
  onClick: () => void
  disabled?: boolean
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'small' | 'medium' | 'large'
}

const Button: React.FC<ButtonProps> = ({
  text,
  onClick,
  disabled = false,
  variant = 'primary',
  size = 'medium',
}) => {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
      disabled={disabled}
    >
      {text}
    </button>
  )
}

使用类型别名定义 Props

tsx
type CardProps = {
  title: string
  children: React.ReactNode
  imageUrl?: string
  footer?: React.ReactElement
  className?: string
}

const Card: React.FC<CardProps> = ({
  title,
  children,
  imageUrl,
  footer,
  className = '',
}) => {
  return (
    <div className={`card ${className}`}>
      {imageUrl && <img src={imageUrl} alt={title} />}
      <div className="card-content">
        <h3>{title}</h3>
        {children}
      </div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  )
}

扩展 HTML 属性

tsx
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label?: string
  error?: string
  helperText?: string
}

const Input: React.FC<InputProps> = ({
  label,
  error,
  helperText,
  ...inputProps
}) => {
  return (
    <div className="input-wrapper">
      {label && <label htmlFor={inputProps.id}>{label}</label>}
      <input
        {...inputProps}
        className={`input ${error ? 'input-error' : ''} ${
          inputProps.className || ''
        }`}
      />
      {error && <span className="error-text">{error}</span>}
      {helperText && !error && (
        <span className="helper-text">{helperText}</span>
      )}
    </div>
  )
}

定义 Ref 类型

使用 useRef Hook

tsx
import React, { useRef, useEffect } from 'react'

// 对于 DOM 元素
const FocusableInput: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    // 自动聚焦
    if (inputRef.current) {
      inputRef.current.focus()
    }
  }, [])

  const handleClick = () => {
    if (inputRef.current) {
      inputRef.current.select()
    }
  }

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="点击按钮选中我" />
      <button onClick={handleClick}>选中输入框文本</button>
    </div>
  )
}

// 对于自定义值(不会触发重新渲染)
const TimerComponent: React.FC = () => {
  const timerRef = useRef<number | null>(null)
  const countRef = useRef<number>(0)

  const startTimer = () => {
    timerRef.current = window.setInterval(() => {
      countRef.current += 1
      console.log(`计数: ${countRef.current}`)
    }, 1000)
  }

  const stopTimer = () => {
    if (timerRef.current) {
      clearInterval(timerRef.current)
      timerRef.current = null
    }
  }

  return (
    <div>
      <button onClick={startTimer}>开始计时</button>
      <button onClick={stopTimer}>停止计时</button>
    </div>
  )
}

使用 forwardRef

tsx
import React, { forwardRef, useImperativeHandle } from 'react'

// 自定义 ref 接口
interface CustomInputRef {
  focus: () => void
  clear: () => void
  getValue: () => string
}

interface CustomInputProps {
  placeholder?: string
  defaultValue?: string
}

const CustomInput = forwardRef<CustomInputRef, CustomInputProps>(
  ({ placeholder = '请输入...', defaultValue = '' }, ref) => {
    const inputRef = useRef<HTMLInputElement>(null)

    useImperativeHandle(ref, () => ({
      focus: () => {
        inputRef.current?.focus()
      },
      clear: () => {
        if (inputRef.current) {
          inputRef.current.value = ''
        }
      },
      getValue: () => {
        return inputRef.current?.value || ''
      },
    }))

    return (
      <input
        ref={inputRef}
        type="text"
        placeholder={placeholder}
        defaultValue={defaultValue}
      />
    )
  },
)

// 使用组件
const ParentComponent: React.FC = () => {
  const inputRef = useRef<CustomInputRef>(null)

  const handleFocus = () => {
    inputRef.current?.focus()
  }

  const handleClear = () => {
    inputRef.current?.clear()
  }

  const handleGetValue = () => {
    const value = inputRef.current?.getValue()
    console.log('输入框值:', value)
  }

  return (
    <div>
      <CustomInput ref={inputRef} placeholder="自定义输入框" />
      <button onClick={handleFocus}>聚焦</button>
      <button onClick={handleClear}>清空</button>
      <button onClick={handleGetValue}>获取值</button>
    </div>
  )
}

定义 Hook 类型

自定义 Hook 类型定义

tsx
import { useState, useEffect, useCallback } from 'react'

// 计数器 Hook
interface UseCounterReturn {
  count: number
  increment: () => void
  decrement: () => void
  reset: () => void
  setCount: (value: number) => void
}

const useCounter = (initialValue: number = 0): UseCounterReturn => {
  const [count, setCount] = useState<number>(initialValue)

  const increment = useCallback(() => {
    setCount((prev) => prev + 1)
  }, [])

  const decrement = useCallback(() => {
    setCount((prev) => prev - 1)
  }, [])

  const reset = useCallback(() => {
    setCount(initialValue)
  }, [initialValue])

  return {
    count,
    increment,
    decrement,
    reset,
    setCount,
  }
}

// 使用示例
const CounterComponent: React.FC = () => {
  const { count, increment, decrement, reset } = useCounter(0)

  return (
    <div>
      <p>当前计数: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>重置</button>
    </div>
  )
}

API 请求 Hook

tsx
import { useState, useEffect } from 'react'

// 通用 API 响应类型
interface ApiResponse<T> {
  data: T | null
  loading: boolean
  error: string | null
}

const useApi = <T,>(url: string): ApiResponse<T> => {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState<boolean>(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true)
        setError(null)

        const response = await fetch(url)
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`)
        }

        const result = (await response.json()) as T
        setData(result)
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred')
      } finally {
        setLoading(false)
      }
    }

    fetchData()
  }, [url])

  return { data, loading, error }
}

// 使用示例
interface User {
  id: number
  name: string
  email: string
}

const UserList: React.FC = () => {
  const { data: users, loading, error } = useApi<User[]>('/api/users')

  if (loading) return <div>加载中...</div>
  if (error) return <div>错误: {error}</div>

  return (
    <div>
      <h2>用户列表</h2>
      {users?.map((user) => (
        <div key={user.id}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      ))}
    </div>
  )
}

表单处理 Hook

tsx
import { useState, useCallback } from 'react'

// 表单字段验证规则
interface ValidationRule {
  required?: boolean
  minLength?: number
  maxLength?: number
  pattern?: RegExp
  validator?: (value: string) => boolean
}

interface FormField<T> {
  value: T
  error: string | null
  touched: boolean
}

interface UseFormReturn<T> {
  fields: { [K in keyof T]: FormField<T[K]> }
  setFieldValue: <K extends keyof T>(name: K, value: T[K]) => void
  setFieldTouched: <K extends keyof T>(name: K, touched: boolean) => void
  validateField: <K extends keyof T>(name: K) => boolean
  validateForm: () => boolean
  resetForm: () => void
  isFormValid: boolean
}

const useForm = <T extends Record<string, any>>(
  initialValues: T,
  validations?: { [K in keyof T]?: ValidationRule },
): UseFormReturn<T> => {
  const [fields, setFields] = useState<{
    [K in keyof T]: FormField<T[K]>
  }>(() => {
    const initialFields = {} as { [K in keyof T]: FormField<T[K]> }
    Object.keys(initialValues).forEach((key) => {
      initialFields[key as keyof T] = {
        value: initialValues[key as keyof T],
        error: null,
        touched: false,
      }
    })
    return initialFields
  })

  const validateField = useCallback(
    <K extends keyof T>(name: K): boolean => {
      if (!validations?.[name]) return true

      const field = fields[name]
      const rules = validations[name]!
      let isValid = true
      let errorMessage: string | null = null

      if (rules.required && !field.value) {
        isValid = false
        errorMessage = '此字段为必填项'
      } else if (
        rules.minLength &&
        String(field.value).length < rules.minLength
      ) {
        isValid = false
        errorMessage = `最小长度为 ${rules.minLength} 个字符`
      } else if (
        rules.maxLength &&
        String(field.value).length > rules.maxLength
      ) {
        isValid = false
        errorMessage = `最大长度为 ${rules.maxLength} 个字符`
      } else if (
        rules.pattern &&
        !rules.pattern.test(String(field.value))
      ) {
        isValid = false
        errorMessage = '格式不正确'
      } else if (
        rules.validator &&
        !rules.validator(String(field.value))
      ) {
        isValid = false
        errorMessage = '验证失败'
      }

      setFields((prev) => ({
        ...prev,
        [name]: {
          ...prev[name],
          error: errorMessage,
        },
      }))

      return isValid
    },
    [fields, validations],
  )

  const setFieldValue = useCallback(
    <K extends keyof T>(name: K, value: T[K]) => {
      setFields((prev) => ({
        ...prev,
        [name]: {
          ...prev[name],
          value,
        },
      }))
    },
    [],
  )

  const setFieldTouched = useCallback(
    <K extends keyof T>(name: K, touched: boolean) => {
      setFields((prev) => ({
        ...prev,
        [name]: {
          ...prev[name],
          touched,
        },
      }))
    },
    [],
  )

  const validateForm = useCallback((): boolean => {
    if (!validations) return true

    let isValid = true
    Object.keys(validations).forEach((key) => {
      const fieldIsValid = validateField(key as keyof T)
      if (!fieldIsValid) {
        isValid = false
      }
    })

    return isValid
  }, [validations, validateField])

  const resetForm = useCallback(() => {
    setFields(() => {
      const resetFields = {} as { [K in keyof T]: FormField<T[K]> }
      Object.keys(initialValues).forEach((key) => {
        resetFields[key as keyof T] = {
          value: initialValues[key as keyof T],
          error: null,
          touched: false,
        }
      })
      return resetFields
    })
  }, [initialValues])

  const isFormValid = Object.values(fields).every((field) => !field.error)

  return {
    fields,
    setFieldValue,
    setFieldTouched,
    validateField,
    validateForm,
    resetForm,
    isFormValid,
  }
}

// 使用示例
interface LoginForm {
  email: string
  password: string
}

const LoginFormComponent: React.FC = () => {
  const {
    fields,
    setFieldValue,
    setFieldTouched,
    validateField,
    isFormValid,
  } = useForm<LoginForm>(
    {
      email: '',
      password: '',
    },
    {
      email: {
        required: true,
        pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
      },
      password: {
        required: true,
        minLength: 6,
      },
    },
  )

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (validateForm()) {
      console.log('表单提交:', {
        email: fields.email.value,
        password: fields.password.value,
      })
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>邮箱:</label>
        <input
          type="email"
          value={fields.email.value}
          onChange={(e) => setFieldValue('email', e.target.value)}
          onBlur={() => {
            setFieldTouched('email', true)
            validateField('email')
          }}
        />
        {fields.email.touched && fields.email.error && (
          <span style={{ color: 'red' }}>{fields.email.error}</span>
        )}
      </div>

      <div>
        <label>密码:</label>
        <input
          type="password"
          value={fields.password.value}
          onChange={(e) => setFieldValue('password', e.target.value)}
          onBlur={() => {
            setFieldTouched('password', true)
            validateField('password')
          }}
        />
        {fields.password.touched && fields.password.error && (
          <span style={{ color: 'red' }}>{fields.password.error}</span>
        )}
      </div>

      <button type="submit" disabled={!isFormValid}>
        登录
      </button>
    </form>
  )
}
React TSX已经加载完毕