code-refactoring

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Ecalyptus React Native Refactoring Skill

Ecalyptus React Native 重构技能

Skill Purpose

技能用途

This skill specializes in refactoring React Native components within the Ecalyptus healthcare mobile application. It focuses on eliminating deeply nested conditionals, optimizing array operations and component re-renders, improving RTK Query usage patterns, and enhancing code readability while maintaining React Compiler compatibility and ESLint compliance.
本技能专注于Ecalyptus医疗健康移动应用中的React Native组件重构。重点在于消除深层嵌套条件语句、优化数组操作和组件重渲染、改进RTK Query使用模式,同时在保持与React Compiler兼容和ESLint合规的前提下提升代码可读性。

Core Refactoring Principles

核心重构原则

Priority Order (Highest to Lowest):
  1. Code readability and maintainability
  2. RTK Query optimization (data flow determines component architecture)
  3. Logical improvements and proper React patterns
  4. Performance optimizations (let React Compiler handle micro-optimizations)
  5. Modern React patterns (when they genuinely improve clarity)
Non-Negotiable Rules:
  • Always use path aliases from
    tsconfig.json
    (
    @components
    ,
    @utils/*
    ,
    @reducers/*
    ,
    @hooks/*
    ,
    @theme
    ,
    @schema/*
    ,
    @types
    ,
    @rootState
    ,
    @config/*
    )
  • Use typed Redux hooks from
    @hooks/app
    (never plain
    useDispatch
    /
    useSelector
    )
  • Reference colors via
    colors.palette.*
    from
    @theme
    (no hardcoded hex values)
  • Maintain React Compiler compatibility (enabled via
    babel-plugin-react-compiler
    )
  • Follow Rules of Hooks (no conditional hook calls, proper dependency arrays)
  • Support React Native environment (verify Web-only APIs before suggesting)
优先级排序(从高到低):
  1. 代码可读性与可维护性
  2. RTK Query优化(数据流决定组件架构)
  3. 逻辑改进与正确的React模式
  4. 性能优化(微优化交由React Compiler处理)
  5. 现代React模式(仅在确实提升清晰度时使用)
不可违背的规则:
  • 始终使用
    tsconfig.json
    中的路径别名(
    @components
    @utils/*
    @reducers/*
    @hooks/*
    @theme
    @schema/*
    @types
    @rootState
    @config/*
  • 使用
    @hooks/app
    中的类型化Redux钩子(绝不使用原生
    useDispatch
    /
    useSelector
  • 通过
    @theme
    中的
    colors.palette.*
    引用颜色(禁止硬编码十六进制值)
  • 保持与React Compiler兼容(通过
    babel-plugin-react-compiler
    启用)
  • 遵循Hooks规则(禁止条件式调用钩子,使用正确的依赖数组)
  • 适配React Native环境(建议使用前先验证Web-only API)

Refactoring Targets

重构目标

1. Deeply Nested Conditionals

1. 深层嵌套条件语句

Problem Pattern - Rendering Logic:
typescript
// Anti-pattern: Nested ternaries and if-else in JSX
return (
  <View>
    {loading ? (
      <Spinner />
    ) : error ? (
      <ErrorView />
    ) : data ? (
      data.length > 0 ? (
        <FlatList data={data} />
      ) : (
        <EmptyState />
      )
    ) : null}
  </View>
)
Refactored Solution:
typescript
import { match, P } from 'ts-pattern'

// Pattern matching with ts-pattern (already in dependencies)
const ContentView = match({ loading, error, data })
  .with({ loading: true }, () => <Spinner />)
  .with({ error: P.not(P.nullish) }, ({ error }) => <ErrorView error={error} />)
  .with({ data: P.when(arr => arr && arr.length > 0) }, ({ data }) => (
    <FlatList data={data} renderItem={renderItem} />
  ))
  .with({ data: P.array() }, () => <EmptyState />)
  .otherwise(() => null)

return <View>{ContentView}</View>
Problem Pattern - Business Logic:
typescript
// Anti-pattern: Nested if-else in functions
function calculateDosage(patient: Patient, medication: Medication) {
  if (patient.age < 18) {
    if (patient.weight < 50) {
      if (medication.type === 'antibiotic') {
        return medication.baseDose * 0.5
      } else {
        return medication.baseDose * 0.7
      }
    } else {
      return medication.baseDose * 0.8
    }
  } else {
    if (patient.hasKidneyDisease) {
      return medication.baseDose * 0.6
    } else {
      return medication.baseDose
    }
  }
}
Refactored Solution:
typescript
// Early returns + extracted logic
function calculateDosage(patient: Patient, medication: Medication) {
  // Handle adult patients with kidney disease first
  if (patient.age >= 18) {
    return patient.hasKidneyDisease 
      ? medication.baseDose * 0.6 
      : medication.baseDose
  }
  
  // Pediatric dosage calculation
  return calculatePediatricDosage(patient, medication)
}

function calculatePediatricDosage(patient: Patient, medication: Medication) {
  const weightFactor = patient.weight < 50 ? 0.5 : 0.8
  const typeFactor = medication.type === 'antibiotic' && patient.weight < 50 ? 1.0 : 1.4
  
  return medication.baseDose * weightFactor * typeFactor
}
问题模式 - 渲染逻辑:
typescript
// 反模式:JSX中使用嵌套三元表达式和if-else
return (
  <View>
    {loading ? (
      <Spinner />
    ) : error ? (
      <ErrorView />
    ) : data ? (
      data.length > 0 ? (
        <FlatList data={data} />
      ) : (
        <EmptyState />
      )
    ) : null}
  </View>
)
重构后的解决方案:
typescript
import { match, P } from 'ts-pattern'

// 使用ts-pattern进行模式匹配(已包含在依赖中)
const ContentView = match({ loading, error, data })
  .with({ loading: true }, () => <Spinner />)
  .with({ error: P.not(P.nullish) }, ({ error }) => <ErrorView error={error} />)
  .with({ data: P.when(arr => arr && arr.length > 0) }, ({ data }) => (
    <FlatList data={data} renderItem={renderItem} />
  ))
  .with({ data: P.array() }, () => <EmptyState />)
  .otherwise(() => null)

return <View>{ContentView}</View>
问题模式 - 业务逻辑:
typescript
// 反模式:函数中使用嵌套if-else
function calculateDosage(patient: Patient, medication: Medication) {
  if (patient.age < 18) {
    if (patient.weight < 50) {
      if (medication.type === 'antibiotic') {
        return medication.baseDose * 0.5
      } else {
        return medication.baseDose * 0.7
      }
    } else {
      return medication.baseDose * 0.8
    }
  } else {
    if (patient.hasKidneyDisease) {
      return medication.baseDose * 0.6
    } else {
      return medication.baseDose
    }
  }
}
重构后的解决方案:
typescript
// 提前返回 + 提取逻辑
function calculateDosage(patient: Patient, medication: Medication) {
  // 先处理成年肾病患者
  if (patient.age >= 18) {
    return patient.hasKidneyDisease 
      ? medication.baseDose * 0.6 
      : medication.baseDose
  }
  
  // 儿科剂量计算
  return calculatePediatricDosage(patient, medication)
}

function calculatePediatricDosage(patient: Patient, medication: Medication) {
  const weightFactor = patient.weight < 50 ? 0.5 : 0.8
  const typeFactor = medication.type === 'antibiotic' && patient.weight < 50 ? 1.0 : 1.4
  
  return medication.baseDose * weightFactor * typeFactor
}

2. Array Mapping Optimization

2. 数组映射优化

Problem Pattern - Unstable References:
typescript
// Anti-pattern: Inline functions and object literals break memoization
function PatientList({ patients }: Props) {
  return (
    <FlatList
      data={patients}
      renderItem={({ item }) => (
        <PatientCard
          patient={item}
          onPress={() => navigate('PatientDetail', { id: item.id })}
          style={{ marginBottom: 8 }}
        />
      )}
    />
  )
}
Refactored Solution:
typescript
import { useCallback, useMemo } from 'react'
import { StyleSheet } from 'react-native-unistyles'

// Stable styles object (created once)
const styles = StyleSheet.create({
  card: { marginBottom: 8 }
})

// Memoized item component (prevents re-renders)
const PatientCardItem = memo(({ 
  patient, 
  onPress 
}: { 
  patient: Patient
  onPress: (id: string) => void 
}) => (
  <PatientCard
    patient={patient}
    onPress={() => onPress(patient.id)}
    style={styles.card}
  />
))

function PatientList({ patients }: Props) {
  const navigation = useNavigation()
  
  // Stable callback reference
  const handlePress = useCallback((id: string) => {
    navigation.navigate('PatientDetail', { id })
  }, [navigation])
  
  // Stable render function
  const renderItem = useCallback(({ item }: { item: Patient }) => (
    <PatientCardItem patient={item} onPress={handlePress} />
  ), [handlePress])
  
  return (
    <FlatList
      data={patients}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
    />
  )
}

// Extract keyExtractor outside component (stable reference)
const keyExtractor = (item: Patient) => item.id
Problem Pattern - Local State in Mapped Components:
typescript
// Anti-pattern: Each card manages its own expanded state
function MedicationList({ medications }: Props) {
  return medications.map(med => (
    <MedicationCard key={med.id} medication={med} />
  ))
}

function MedicationCard({ medication }: { medication: Medication }) {
  const [expanded, setExpanded] = useState(false)
  
  // Error: When parent re-renders, this state resets
  return (
    <Pressable onPress={() => setExpanded(!expanded)}>
      <Text>{medication.name}</Text>
      {expanded && <Text>{medication.dosage}</Text>}
    </Pressable>
  )
}
Refactored Solution:
typescript
// Lift state to parent OR use unique keys
function MedicationList({ medications }: Props) {
  const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
  
  const toggleExpanded = useCallback((id: string) => {
    setExpandedIds(prev => {
      const next = new Set(prev)
      next.has(id) ? next.delete(id) : next.add(id)
      return next
    })
  }, [])
  
  return medications.map(med => (
    <MedicationCard
      key={med.id}
      medication={med}
      expanded={expandedIds.has(med.id)}
      onToggle={toggleExpanded}
    />
  ))
}

// Pure component with no internal state
const MedicationCard = memo(({ 
  medication, 
  expanded, 
  onToggle 
}: MedicationCardProps) => (
  <Pressable onPress={() => onToggle(medication.id)}>
    <Text>{medication.name}</Text>
    {expanded && <Text>{medication.dosage}</Text>}
  </Pressable>
))
问题模式 - 不稳定引用:
typescript
// 反模式:内联函数和对象字面量破坏记忆化
function PatientList({ patients }: Props) {
  return (
    <FlatList
      data={patients}
      renderItem={({ item }) => (
        <PatientCard
          patient={item}
          onPress={() => navigate('PatientDetail', { id: item.id })}
          style={{ marginBottom: 8 }}
        />
      )}
    />
  )
}
重构后的解决方案:
typescript
import { useCallback, useMemo } from 'react'
import { StyleSheet } from 'react-native-unistyles'

// 稳定的样式对象(仅创建一次)
const styles = StyleSheet.create({
  card: { marginBottom: 8 }
})

// 记忆化的子项组件(防止重渲染)
const PatientCardItem = memo(({ 
  patient, 
  onPress 
}: { 
  patient: Patient
  onPress: (id: string) => void 
}) => (
  <PatientCard
    patient={patient}
    onPress={() => onPress(patient.id)}
    style={styles.card}
  />
))

function PatientList({ patients }: Props) {
  const navigation = useNavigation()
  
  // 稳定的回调引用
  const handlePress = useCallback((id: string) => {
    navigation.navigate('PatientDetail', { id })
  }, [navigation])
  
  // 稳定的渲染函数
  const renderItem = useCallback(({ item }: { item: Patient }) => (
    <PatientCardItem patient={item} onPress={handlePress} />
  ), [handlePress])
  
  return (
    <FlatList
      data={patients}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
    />
  )
}

// 将keyExtractor提取到组件外部(稳定引用)
const keyExtractor = (item: Patient) => item.id
问题模式 - 映射组件中的本地状态:
typescript
// 反模式:每个卡片管理自己的展开状态
function MedicationList({ medications }: Props) {
  return medications.map(med => (
    <MedicationCard key={med.id} medication={med} />
  ))
}

function MedicationCard({ medication }: { medication: Medication }) {
  const [expanded, setExpanded] = useState(false)
  
  // 错误:父组件重渲染时,该状态会重置
  return (
    <Pressable onPress={() => setExpanded(!expanded)}>
      <Text>{medication.name}</Text>
      {expanded && <Text>{medication.dosage}</Text>}
    </Pressable>
  )
}
重构后的解决方案:
typescript
// 将状态提升到父组件 OR 使用唯一键
function MedicationList({ medications }: Props) {
  const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
  
  const toggleExpanded = useCallback((id: string) => {
    setExpandedIds(prev => {
      const next = new Set(prev)
      next.has(id) ? next.delete(id) : next.add(id)
      return next
    })
  }, [])
  
  return medications.map(med => (
    <MedicationCard
      key={med.id}
      medication={med}
      expanded={expandedIds.has(med.id)}
      onToggle={toggleExpanded}
    />
  ))
}

// 无内部状态的纯组件
const MedicationCard = memo(({ 
  medication, 
  expanded, 
  onToggle 
}: MedicationCardProps) => (
  <Pressable onPress={() => onToggle(medication.id)}>
    <Text>{medication.name}</Text>
    {expanded && <Text>{medication.dosage}</Text>}
  </Pressable>
))

3. RTK Query Hook Optimization

3. RTK Query 钩子优化

Problem Pattern - Over-Fetching Data:
typescript
// Anti-pattern: Component re-renders when ANY field in patient changes
function VitalSignsDisplay({ patientId }: Props) {
  const { data: patient } = useGetPatientQuery(patientId)
  
  // Re-renders even when only patient.name changes (not vitals)
  return (
    <View>
      <Text>BP: {patient?.latestVitals?.bloodPressure}</Text>
      <Text>HR: {patient?.latestVitals?.heartRate}</Text>
    </View>
  )
}
Refactored Solution:
typescript
// Cherry-pick only needed fields with selectFromResult
function VitalSignsDisplay({ patientId }: Props) {
  const { vitals } = useGetPatientQuery(patientId, {
    selectFromResult: ({ data }) => ({
      vitals: data?.latestVitals
    })
  })
  
  // Only re-renders when latestVitals actually changes
  return (
    <View>
      <Text>BP: {vitals?.bloodPressure ?? '-'}</Text>
      <Text>HR: {vitals?.heartRate ?? '-'}</Text>
    </View>
  )
}
Problem Pattern - Conditional Fetching:
typescript
// Anti-pattern: Fetches even when ID is undefined
function PatientDetails({ patientId }: { patientId?: string }) {
  const { data, isLoading } = useGetPatientQuery(patientId ?? '')
  
  if (!patientId) return <Text>Select a patient</Text>
  // Query already executed with empty string
}
Refactored Solution:
typescript
import { skipToken } from '@reduxjs/toolkit/query'

// Use skipToken to prevent unnecessary requests
function PatientDetails({ patientId }: { patientId?: string }) {
  const { data, isLoading } = useGetPatientQuery(patientId ?? skipToken)
  
  if (!patientId) return <Text>Select a patient</Text>
  if (isLoading) return <ActivityIndicator />
  
  return <PatientCard patient={data} />
}
Problem Pattern - Missing Cache Invalidation:
typescript
// Anti-pattern: Manual refetch after mutation
const [updateVitals] = useUpdateVitalsMutation()
const { refetch } = useGetPatientQuery(patientId)

const handleSubmit = async (vitals: VitalsInput) => {
  await updateVitals({ patientId, vitals })
  refetch() // Manual refetch is fragile
}
Refactored Solution:
typescript
// Properly configured mutation invalidates cache automatically
const vitalsApi = mainApi.injectEndpoints({
  endpoints: builder => ({
    updateVitals: builder.mutation<void, UpdateVitalsParams>({
      query: ({ patientId, vitals }) => ({
        url: `/patients/${patientId}/vitals`,
        method: 'POST',
        body: vitals
      }),
      // Automatic cache invalidation
      invalidatesTags: (result, error, { patientId }) => [
        { type: 'Patient', id: patientId },
        'PatientVitals'
      ]
    }),
    getPatient: builder.query<Patient, string>({
      query: (id) => `/patients/${id}`,
      providesTags: (result, error, id) => [
        { type: 'Patient', id }
      ]
    })
  })
})

// Usage - no manual refetch needed
const [updateVitals] = useUpdateVitalsMutation()

const handleSubmit = async (vitals: VitalsInput) => {
  await updateVitals({ patientId, vitals })
  // Cache automatically refetches due to invalidatesTags
}
Problem Pattern - Polling Without Cleanup:
typescript
// Anti-pattern: Continuous polling even when screen is hidden
function LiveMonitorScreen({ patientId }: Props) {
  const { data } = useGetVitalsQuery(patientId, {
    pollingInterval: 5000 // Polls forever
  })
}
Refactored Solution:
typescript
import { useFocusEffect } from '@react-navigation/native'
import { useRef } from 'react'

function LiveMonitorScreen({ patientId }: Props) {
  const [pollingInterval, setPollingInterval] = useState(0)
  
  // Start/stop polling based on screen focus
  useFocusEffect(
    useCallback(() => {
      setPollingInterval(5000)
      return () => setPollingInterval(0)
    }, [])
  )
  
  const { data } = useGetVitalsQuery(patientId, {
    pollingInterval,
    skip: !pollingInterval // Don't fetch when interval is 0
  })
  
  return <VitalsDisplay vitals={data} />
}
问题模式 - 过度获取数据:
typescript
// 反模式:patient中任意字段变化时组件都会重渲染
function VitalSignsDisplay({ patientId }: Props) {
  const { data: patient } = useGetPatientQuery(patientId)
  
  // 即使只有patient.name变化(而非生命体征)也会重渲染
  return (
    <View>
      <Text>BP: {patient?.latestVitals?.bloodPressure}</Text>
      <Text>HR: {patient?.latestVitals?.heartRate}</Text>
    </View>
  )
}
重构后的解决方案:
typescript
// 使用selectFromResult仅挑选所需字段
function VitalSignsDisplay({ patientId }: Props) {
  const { vitals } = useGetPatientQuery(patientId, {
    selectFromResult: ({ data }) => ({
      vitals: data?.latestVitals
    })
  })
  
  // 仅当latestVitals实际变化时才重渲染
  return (
    <View>
      <Text>BP: {vitals?.bloodPressure ?? '-'}</Text>
      <Text>HR: {vitals?.heartRate ?? '-'}</Text>
    </View>
  )
}
问题模式 - 条件式请求:
typescript
// 反模式:即使ID未定义也会发起请求
function PatientDetails({ patientId }: { patientId?: string }) {
  const { data, isLoading } = useGetPatientQuery(patientId ?? '')
  
  if (!patientId) return <Text>选择一位患者</Text>
  // 已使用空字符串执行了查询
}
重构后的解决方案:
typescript
import { skipToken } from '@reduxjs/toolkit/query'

// 使用skipToken避免不必要的请求
function PatientDetails({ patientId }: { patientId?: string }) {
  const { data, isLoading } = useGetPatientQuery(patientId ?? skipToken)
  
  if (!patientId) return <Text>选择一位患者</Text>
  if (isLoading) return <ActivityIndicator />
  
  return <PatientCard patient={data} />
}
问题模式 - 缺失缓存失效:
typescript
// 反模式:变更后手动重新获取
const [updateVitals] = useUpdateVitalsMutation()
const { refetch } = useGetPatientQuery(patientId)

const handleSubmit = async (vitals: VitalsInput) => {
  await updateVitals({ patientId, vitals })
  refetch() // 手动重新获取不可靠
}
重构后的解决方案:
typescript
// 正确配置的变更会自动使缓存失效
const vitalsApi = mainApi.injectEndpoints({
  endpoints: builder => ({
    updateVitals: builder.mutation<void, UpdateVitalsParams>({
      query: ({ patientId, vitals }) => ({
        url: `/patients/${patientId}/vitals`,
        method: 'POST',
        body: vitals
      }),
      // 自动缓存失效
      invalidatesTags: (result, error, { patientId }) => [
        { type: 'Patient', id: patientId },
        'PatientVitals'
      ]
    }),
    getPatient: builder.query<Patient, string>({
      query: (id) => `/patients/${id}`,
      providesTags: (result, error, id) => [
        { type: 'Patient', id }
      ]
    })
  })
})

// 使用方式 - 无需手动重新获取
const [updateVitals] = useUpdateVitalsMutation()

const handleSubmit = async (vitals: VitalsInput) => {
  await updateVitals({ patientId, vitals })
  // 由于invalidatesTags,缓存会自动重新获取
}
问题模式 - 无清理的轮询:
typescript
// 反模式:即使屏幕隐藏仍持续轮询
function LiveMonitorScreen({ patientId }: Props) {
  const { data } = useGetVitalsQuery(patientId, {
    pollingInterval: 5000 // 永久轮询
  })
}
重构后的解决方案:
typescript
import { useFocusEffect } from '@react-navigation/native'
import { useRef } from 'react'

function LiveMonitorScreen({ patientId }: Props) {
  const [pollingInterval, setPollingInterval] = useState(0)
  
  // 根据屏幕焦点启动/停止轮询
  useFocusEffect(
    useCallback(() => {
      setPollingInterval(5000)
      return () => setPollingInterval(0)
    }, [])
  )
  
  const { data } = useGetVitalsQuery(patientId, {
    pollingInterval,
    skip: !pollingInterval // 当间隔为0时不发起请求
  })
  
  return <VitalsDisplay vitals={data} />
}

4. Unnecessary Hook Usage

4. 不必要的Hook使用

Problem Pattern - Redundant useMemo:
typescript
// Anti-pattern: Memoizing primitive calculations
function DosageCalculator({ weight, medication }: Props) {
  const dosage = useMemo(() => {
    return weight * medication.dosePerKg
  }, [weight, medication.dosePerKg])
  
  // Simple arithmetic doesn't need memoization
}
Refactored Solution:
typescript
// Direct calculation (React Compiler optimizes this)
function DosageCalculator({ weight, medication }: Props) {
  const dosage = weight * medication.dosePerKg
  
  return <Text>{dosage}mg</Text>
}
Problem Pattern - useEffect for Derived State:
typescript
// Anti-pattern: Synchronizing state with useEffect
function PatientSummary({ patient }: Props) {
  const [fullName, setFullName] = useState('')
  
  useEffect(() => {
    setFullName(`${patient.firstName} ${patient.lastName}`)
  }, [patient.firstName, patient.lastName])
  
  return <Text>{fullName}</Text>
}
Refactored Solution:
typescript
// Compute during render (no synchronization needed)
function PatientSummary({ patient }: Props) {
  const fullName = `${patient.firstName} ${patient.lastName}`
  
  return <Text>{fullName}</Text>
}
Problem Pattern - useCallback Without Benefit:
typescript
// Anti-pattern: Wrapping stable functions
function FormScreen() {
  const navigation = useNavigation()
  
  const goBack = useCallback(() => {
    navigation.goBack()
  }, [navigation])
  
  // navigation is already stable from React Navigation
}
Refactored Solution:
typescript
// Direct inline handler (React Compiler handles this)
function FormScreen() {
  const navigation = useNavigation()
  
  return (
    <Button onPress={() => navigation.goBack()}>
      Cancel
    </Button>
  )
}
问题模式 - 冗余的useMemo:
typescript
// 反模式:对基础计算进行记忆化
function DosageCalculator({ weight, medication }: Props) {
  const dosage = useMemo(() => {
    return weight * medication.dosePerKg
  }, [weight, medication.dosePerKg])
  
  // 简单算术运算无需记忆化
}
重构后的解决方案:
typescript
// 直接计算(React Compiler会优化此逻辑)
function DosageCalculator({ weight, medication }: Props) {
  const dosage = weight * medication.dosePerKg
  
  return <Text>{dosage}mg</Text>
}
问题模式 - 使用useEffect处理派生状态:
typescript
// 反模式:使用useEffect同步状态
function PatientSummary({ patient }: Props) {
  const [fullName, setFullName] = useState('')
  
  useEffect(() => {
    setFullName(`${patient.firstName} ${patient.lastName}`)
  }, [patient.firstName, patient.lastName])
  
  return <Text>{fullName}</Text>
}
重构后的解决方案:
typescript
// 在渲染时直接计算(无需同步)
function PatientSummary({ patient }: Props) {
  const fullName = `${patient.firstName} ${patient.lastName}`
  
  return <Text>{fullName}</Text>
}
问题模式 - 无收益的useCallback:
typescript
// 反模式:包裹稳定函数
function FormScreen() {
  const navigation = useNavigation()
  
  const goBack = useCallback(() => {
    navigation.goBack()
  }, [navigation])
  
  // navigation在React Navigation中已经是稳定引用
}
重构后的解决方案:
typescript
// 直接内联处理函数(React Compiler会处理此逻辑)
function FormScreen() {
  const navigation = useNavigation()
  
  return (
    <Button onPress={() => navigation.goBack()}>
      取消
    </Button>
  )
}

5. Modern React Patterns (React 19+)

5. 现代React模式(React 19+)

Pattern - use() Hook for Promises:
typescript
// Old pattern: useEffect + loading state
function PatientLoader({ patientId }: Props) {
  const [patient, setPatient] = useState<Patient | null>(null)
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    fetchPatient(patientId).then(data => {
      setPatient(data)
      setLoading(false)
    })
  }, [patientId])
  
  if (loading) return <Spinner />
  return <PatientCard patient={patient} />
}
Modern Solution (React 19):
typescript
import { use, Suspense } from 'react'

// use() unwraps promises and integrates with Suspense
function PatientLoader({ patientPromise }: { patientPromise: Promise<Patient> }) {
  const patient = use(patientPromise)
  return <PatientCard patient={patient} />
}

// Parent component
function PatientScreen({ patientId }: Props) {
  const patientPromise = fetchPatient(patientId)
  
  return (
    <Suspense fallback={<Spinner />}>
      <PatientLoader patientPromise={patientPromise} />
    </Suspense>
  )
}
Pattern - useActionState for Forms (React 19):
typescript
// Old pattern: useState + async handler
function VitalsForm({ patientId }: Props) {
  const [pending, setPending] = useState(false)
  const [error, setError] = useState<string | null>(null)
  
  const handleSubmit = async (data: VitalsInput) => {
    setPending(true)
    setError(null)
    try {
      await submitVitals(patientId, data)
    } catch (err) {
      setError(err.message)
    } finally {
      setPending(false)
    }
  }
}
Modern Solution (React 19):
typescript
import { useActionState } from 'react'

// useActionState handles pending state and errors automatically
function VitalsForm({ patientId }: Props) {
  const [state, submitAction, isPending] = useActionState(
    async (prevState: FormState, formData: FormData) => {
      try {
        await submitVitals(patientId, formData)
        return { success: true, error: null }
      } catch (err) {
        return { success: false, error: err.message }
      }
    },
    { success: false, error: null }
  )
  
  return (
    <form action={submitAction}>
      {state.error && <Text>{state.error}</Text>}
      <Button disabled={isPending}>
        {isPending ? 'Submitting...' : 'Submit'}
      </Button>
    </form>
  )
}
Pattern - ref Callback Cleanup (React 19):
typescript
// Old pattern: useEffect for DOM operations
function VideoPlayer({ src }: Props) {
  const videoRef = useRef<Video>(null)
  
  useEffect(() => {
    const player = videoRef.current
    if (player) {
      player.play()
      return () => player.pause()
    }
  }, [])
  
  return <Video ref={videoRef} source={src} />
}
Modern Solution (React 19):
typescript
// ref callback with cleanup function
function VideoPlayer({ src }: Props) {
  return (
    <Video
      ref={(player) => {
        if (player) {
          player.play()
          return () => player.pause() // Cleanup
        }
      }}
      source={src}
    />
  )
}
模式 - 使用use() Hook处理Promise:
typescript
// 旧模式:useEffect + 加载状态
function PatientLoader({ patientId }: Props) {
  const [patient, setPatient] = useState<Patient | null>(null)
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    fetchPatient(patientId).then(data => {
      setPatient(data)
      setLoading(false)
    })
  }, [patientId])
  
  if (loading) return <Spinner />
  return <PatientCard patient={patient} />
}
现代解决方案(React 19):
typescript
import { use, Suspense } from 'react'

// use() 会解包Promise并与Suspense集成
function PatientLoader({ patientPromise }: { patientPromise: Promise<Patient> }) {
  const patient = use(patientPromise)
  return <PatientCard patient={patient} />
}

// 父组件
function PatientScreen({ patientId }: Props) {
  const patientPromise = fetchPatient(patientId)
  
  return (
    <Suspense fallback={<Spinner />}>
      <PatientLoader patientPromise={patientPromise} />
    </Suspense>
  )
}
模式 - 使用useActionState处理表单(React 19):
typescript
// 旧模式:useState + 异步处理函数
function VitalsForm({ patientId }: Props) {
  const [pending, setPending] = useState(false)
  const [error, setError] = useState<string | null>(null)
  
  const handleSubmit = async (data: VitalsInput) => {
    setPending(true)
    setError(null)
    try {
      await submitVitals(patientId, data)
    } catch (err) {
      setError(err.message)
    } finally {
      setPending(false)
    }
  }
}
现代解决方案(React 19):
typescript
import { useActionState } from 'react'

// useActionState会自动处理等待状态和错误
function VitalsForm({ patientId }: Props) {
  const [state, submitAction, isPending] = useActionState(
    async (prevState: FormState, formData: FormData) => {
      try {
        await submitVitals(patientId, formData)
        return { success: true, error: null }
      } catch (err) {
        return { success: false, error: err.message }
      }
    },
    { success: false, error: null }
  )
  
  return (
    <form action={submitAction}>
      {state.error && <Text>{state.error}</Text>}
      <Button disabled={isPending}>
        {isPending ? '提交中...' : '提交'}
      </Button>
    </form>
  )
}
模式 - ref回调清理(React 19):
typescript
// 旧模式:使用useEffect处理DOM操作
function VideoPlayer({ src }: Props) {
  const videoRef = useRef<Video>(null)
  
  useEffect(() => {
    const player = videoRef.current
    if (player) {
      player.play()
      return () => player.pause()
    }
  }, [])
  
  return <Video ref={videoRef} source={src} />
}
现代解决方案(React 19):
typescript
// 带清理函数的ref回调
function VideoPlayer({ src }: Props) {
  return (
    <Video
      ref={(player) => {
        if (player) {
          player.play()
          return () => player.pause() // 清理逻辑
        }
      }}
      source={src}
    />
  )
}

Refactoring Workflow

重构工作流

Step 1: Analyze Current Pattern

步骤1:分析当前模式

  • Identify the anti-pattern (nested conditionals, unoptimized loops, etc.)
  • Check if the pattern violates React rules or ESLint config
  • Assess impact on component re-renders
  • 识别反模式(嵌套条件语句、未优化的循环等)
  • 检查该模式是否违反React规则或ESLint配置
  • 评估对组件重渲染的影响

Step 2: Check RTK Query Usage

步骤2:检查RTK Query使用

  • Is data being over-fetched? → Use
    selectFromResult
  • Are there conditional queries? → Use
    skipToken
  • Is cache invalidation configured? → Add
    invalidatesTags
    /
    providesTags
  • Is polling controlled? → Tie to screen focus with
    useFocusEffect
  • 是否存在过度获取数据?→ 使用
    selectFromResult
  • 是否有条件式查询?→ 使用
    skipToken
  • 是否配置了缓存失效?→ 添加
    invalidatesTags
    /
    providesTags
  • 轮询是否可控?→ 通过
    useFocusEffect
    关联屏幕焦点

Step 3: Evaluate Hook Necessity

步骤3:评估Hook必要性

  • Is
    useMemo
    wrapping simple calculations? → Remove
  • Is
    useCallback
    wrapping stable references? → Remove
  • Is
    useEffect
    creating derived state? → Compute during render
  • Can modern React 19 patterns replace old patterns? → Suggest upgrade if clearer
  • useMemo
    是否包裹了简单计算?→ 移除
  • useCallback
    是否包裹了稳定引用?→ 移除
  • useEffect
    是否用于创建派生状态?→ 在渲染时计算
  • 现代React 19模式能否替代旧模式?→ 如果更清晰则建议升级

Step 4: Apply Refactoring

步骤4:应用重构

  • Extract complex logic into helper functions
  • Use
    ts-pattern
    for nested conditionals in rendering
  • Use early returns for nested conditionals in functions
  • Stabilize references with
    useCallback
    only when passing to memoized children
  • Memoize expensive computations with clear performance benefit
  • Lift state when mapped components need coordination
  • 将复杂逻辑提取到辅助函数中
  • 在渲染中使用
    ts-pattern
    处理嵌套条件语句
  • 在函数中使用提前返回处理嵌套条件语句
  • 仅当传递给记忆化子组件时,使用
    useCallback
    稳定引用
  • 仅对有明显性能收益的昂贵计算进行记忆化
  • 当映射组件需要协同工作时提升状态

Step 5: Verify Compliance

步骤5:验证合规性

  • All imports use path aliases (
    @components
    ,
    @utils/*
    , etc.)
  • Colors reference
    colors.palette.*
    from
    @theme
  • Redux hooks are typed (
    useAppDispatch
    ,
    useAppSelector
    )
  • No Rules of Hooks violations
  • React Compiler compatible (no manual object identity preservation unless necessary)
  • ESLint passes
  • 所有导入都使用路径别名(
    @components
    @utils/*
    等)
  • 颜色引用来自
    @theme
    colors.palette.*
  • Redux钩子是类型化的(
    useAppDispatch
    useAppSelector
  • 无Hooks规则违反
  • 与React Compiler兼容(除非必要,否则无需手动保持对象标识)
  • ESLint检查通过

Code Generation Guidelines

代码生成指南

When suggesting refactored code:
Always Include:
  • Import statements with correct path aliases
  • Type annotations for function parameters and returns
  • Comments explaining WHY the refactor improves the code
  • Before/after comparison when helpful
Always Explain:
  • The specific anti-pattern being fixed
  • How the refactor improves readability/performance
  • Trade-offs (if any) of the new approach
  • Which React/RTK Query pattern is being applied
Never:
  • Use relative imports like
    ../../components
  • Hardcode colors (always use
    colors.palette.*
    )
  • Suggest React Native incompatible patterns (e.g.,
    Suspense
    for data fetching is experimental in RN)
  • Use plain Redux hooks (
    useDispatch
    ,
    useSelector
    )
  • Suggest patterns that conflict with React Compiler
当建议重构后的代码时:
必须包含:
  • 使用正确路径别名的导入语句
  • 函数参数和返回值的类型注解
  • 解释重构为何提升代码质量的注释
  • 必要时提供前后对比
必须说明:
  • 正在修复的具体反模式
  • 重构如何提升可读性/性能
  • 新方案的权衡(如果有)
  • 应用的是哪种React/RTK Query模式
禁止:
  • 使用
    ../../components
    这类相对导入
  • 硬编码颜色(始终使用
    colors.palette.*
  • 建议与React Native不兼容的模式(例如RN中数据获取的Suspense是实验性的)
  • 使用原生Redux钩子(
    useDispatch
    useSelector
  • 建议与React Compiler冲突的模式

Example Refactoring Request/Response

示例重构请求/响应

User Request: "This component has nested ifs and the list re-renders too much when I update a single item's state"
Assistant Response: I can see two issues here. First, the nested conditionals in your render logic make it hard to understand the component's states. We can refactor this using
ts-pattern
for clearer state matching. Second, your mapped list items maintain their own expanded state, which gets lost on parent re-renders. We should lift that state to the parent component.
Let me show you the refactored version with explanations:
typescript
import { match, P } from 'ts-pattern'
import { useCallback, useState, memo } from 'react'
import { useGetPatientsQuery } from '@reducers/patientSlice'
import { colors } from '@theme'

// Lift state to parent so it survives re-renders
function PatientList() {
  const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
  const { patients, isLoading, error } = useGetPatientsQuery(undefined, {
    // Only re-render when patients array changes, not when unrelated data updates
    selectFromResult: ({ data, isLoading, error }) => ({
      patients: data?.patients ?? [],
      isLoading,
      error
    })
  })
  
  // Stable callback for toggling expansion
  const toggleExpanded = useCallback((id: string) => {
    setExpandedIds(prev => {
      const next = new Set(prev)
      next.has(id) ? next.delete(id) : next.add(id)
      return next
    })
  }, [])
  
  // Clear, readable state matching with ts-pattern
  const Content = match({ isLoading, error, patients })
    .with({ isLoading: true }, () => <ActivityIndicator />)
    .with({ error: P.not(P.nullish) }, ({ error }) => (
      <ErrorView message={error.message} />
    ))
    .with({ patients: P.when(arr => arr.length > 0) }, ({ patients }) => (
      <FlatList
        data={patients}
        renderItem={({ item }) => (
          <PatientCard
            patient={item}
            expanded={expandedIds.has(item.id)}
            onToggle={toggleExpanded}
          />
        )}
        keyExtractor={keyExtractor}
      />
    ))
    .with({ patients: P.array() }, () => (
      <EmptyState message="No patients found" />
    ))
    .otherwise(() => null)
  
  return <View style={styles.container}>{Content}</View>
}

// Memoized to prevent re-renders when other list items change
const PatientCard = memo(({ patient, expanded, onToggle }: PatientCardProps) => (
  <Pressable
    onPress={() => onToggle(patient.id)}
    style={styles.card}
  >
    <Text style={styles.name}>{patient.name}</Text>
    {expanded && (
      <Text style={styles.details}>
        Age: {patient.age} | MRN: {patient.mrn}
      </Text>
    )}
  </Pressable>
))

// Extract keyExtractor outside component for stable reference
const keyExtractor = (item: Patient) => item.id

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: colors.palette.white
  },
  card: {
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: colors.palette.neutral300
  },
  name: {
    fontSize: 16,
    fontFamily: 'Manrope-SemiBold',
    color: colors.palette.neutral900
  },
  details: {
    marginTop: 8,
    fontSize: 14,
    fontFamily: 'Manrope-Regular',
    color: colors.palette.neutral600
  }
})
This refactor accomplishes several things:
First, the
ts-pattern
match eliminates four levels of nested ternaries, making each state (loading, error, data, empty) explicit and easy to reason about. You can now see at a glance what renders in each scenario.
Second, the expanded state now lives in the parent as a Set of IDs, which persists across re-renders and allows you to track multiple expanded items efficiently. The Set data structure gives you O(1) lookup performance when checking if an item is expanded.
Third, the RTK Query
selectFromResult
ensures this component only re-renders when the patients array actually changes, not when other fields in the API response update. This is crucial for performance in lists.
Fourth, the PatientCard is memoized and receives only the props it needs, preventing unnecessary re-renders when sibling items update their state. The stable
toggleExpanded
callback and
keyExtractor
function ensure the memoization isn't broken by new function references on each render.
The React Compiler will handle optimizing the rest, so we don't need additional memoization beyond these strategic points. Does this approach make sense for your use case?
用户请求: "这个组件有嵌套的if语句,而且当我更新单个子项的状态时,整个列表都会重渲染"
助手响应: 我发现这里有两个问题。首先,渲染逻辑中的嵌套条件语句让组件的状态难以理解。我们可以使用
ts-pattern
进行重构,实现更清晰的状态匹配。其次,你的映射列表项各自维护自己的展开状态,这会在父组件重渲染时丢失。我们应该将状态提升到父组件中。
让我为你展示重构后的版本及说明:
typescript
import { match, P } from 'ts-pattern'
import { useCallback, useState, memo } from 'react'
import { useGetPatientsQuery } from '@reducers/patientSlice'
import { colors } from '@theme'

// 将状态提升到父组件,使其在重渲染时得以保留
function PatientList() {
  const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
  const { patients, isLoading, error } = useGetPatientsQuery(undefined, {
    // 仅当patients数组变化时才重渲染,而非无关数据更新时
    selectFromResult: ({ data, isLoading, error }) => ({
      patients: data?.patients ?? [],
      isLoading,
      error
    })
  })
  
  // 稳定的展开切换回调
  const toggleExpanded = useCallback((id: string) => {
    setExpandedIds(prev => {
      const next = new Set(prev)
      next.has(id) ? next.delete(id) : next.add(id)
      return next
    })
  }, [])
  
  // 使用ts-pattern实现清晰、可读的状态匹配
  const Content = match({ isLoading, error, patients })
    .with({ isLoading: true }, () => <ActivityIndicator />)
    .with({ error: P.not(P.nullish) }, ({ error }) => (
      <ErrorView message={error.message} />
    ))
    .with({ patients: P.when(arr => arr.length > 0) }, ({ patients }) => (
      <FlatList
        data={patients}
        renderItem={({ item }) => (
          <PatientCard
            patient={item}
            expanded={expandedIds.has(item.id)}
            onToggle={toggleExpanded}
          />
        )}
        keyExtractor={keyExtractor}
      />
    ))
    .with({ patients: P.array() }, () => (
      <EmptyState message="未找到患者" />
    ))
    .otherwise(() => null)
  
  return <View style={styles.container}>{Content}</View>
}

// 记忆化处理,防止其他列表项更新时触发重渲染
const PatientCard = memo(({ patient, expanded, onToggle }: PatientCardProps) => (
  <Pressable
    onPress={() => onToggle(patient.id)}
    style={styles.card}
  >
    <Text style={styles.name}>{patient.name}</Text>
    {expanded && (
      <Text style={styles.details}>
        年龄: {patient.age} | 病历号: {patient.mrn}
      </Text>
    )}
  </Pressable>
))

// 将keyExtractor提取到组件外部,确保引用稳定
const keyExtractor = (item: Patient) => item.id

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: colors.palette.white
  },
  card: {
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: colors.palette.neutral300
  },
  name: {
    fontSize: 16,
    fontFamily: 'Manrope-SemiBold',
    color: colors.palette.neutral900
  },
  details: {
    marginTop: 8,
    fontSize: 14,
    fontFamily: 'Manrope-Regular',
    color: colors.palette.neutral600
  }
})
这次重构实现了以下几点:
首先,
ts-pattern
的匹配消除了四层嵌套的三元表达式,让每个状态(加载中、错误、有数据、空数据)都变得明确且易于理解。现在你可以一眼看到每种场景下的渲染内容。
其次,展开状态现在以ID集合的形式存储在父组件中,这会在重渲染时保留,并且能让你高效地跟踪多个展开的子项。集合数据结构在检查子项是否展开时提供O(1)的查找性能。
第三,RTK Query的
selectFromResult
确保该组件仅在patients数组实际变化时才重渲染,而非API响应中其他字段更新时。这对列表的性能至关重要。
第四,PatientCard组件被记忆化,且仅接收所需的props,防止兄弟项更新状态时触发不必要的重渲染。稳定的
toggleExpanded
回调和
keyExtractor
函数确保记忆化不会因每次渲染时的新函数引用而失效。
剩下的优化工作会由React Compiler处理,因此除了这些关键节点外,我们不需要额外的记忆化处理。这个方案是否符合你的使用场景?