--- name: code-refactoring description: Guide for code refactoring, use this skill to guide you when user asked to refactor a components or functions and when an implementation of a plan requiring a code refactoring. --- # Ecalyptus React Native Refactoring Skill ## 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. ## 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) ## Refactoring Targets ### 1. Deeply Nested Conditionals **Problem Pattern - Rendering Logic:** ```typescript // Anti-pattern: Nested ternaries and if-else in JSX return ( {loading ? ( ) : error ? ( ) : data ? ( data.length > 0 ? ( ) : ( ) ) : null} ) ``` **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 }, () => ) .with({ error: P.not(P.nullish) }, ({ error }) => ) .with({ data: P.when(arr => arr && arr.length > 0) }, ({ data }) => ( )) .with({ data: P.array() }, () => ) .otherwise(() => null) return {ContentView} ``` **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 } ``` ### 2. Array Mapping Optimization **Problem Pattern - Unstable References:** ```typescript // Anti-pattern: Inline functions and object literals break memoization function PatientList({ patients }: Props) { return ( ( 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 }) => ( 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 }) => ( ), [handlePress]) return ( ) } // 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 => ( )) } function MedicationCard({ medication }: { medication: Medication }) { const [expanded, setExpanded] = useState(false) // Error: When parent re-renders, this state resets return ( setExpanded(!expanded)}> {medication.name} {expanded && {medication.dosage}} ) } ``` **Refactored Solution:** ```typescript // Lift state to parent OR use unique keys function MedicationList({ medications }: Props) { const [expandedIds, setExpandedIds] = useState>(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 => ( )) } // Pure component with no internal state const MedicationCard = memo(({ medication, expanded, onToggle }: MedicationCardProps) => ( onToggle(medication.id)}> {medication.name} {expanded && {medication.dosage}} )) ``` ### 3. RTK Query Hook Optimization **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 ( BP: {patient?.latestVitals?.bloodPressure} HR: {patient?.latestVitals?.heartRate} ) } ``` **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 ( BP: {vitals?.bloodPressure ?? '-'} HR: {vitals?.heartRate ?? '-'} ) } ``` **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 Select a patient // 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 Select a patient if (isLoading) return return } ``` **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({ 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({ 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 } ``` ### 4. Unnecessary Hook Usage **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 {dosage}mg } ``` **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 {fullName} } ``` **Refactored Solution:** ```typescript // Compute during render (no synchronization needed) function PatientSummary({ patient }: Props) { const fullName = `${patient.firstName} ${patient.lastName}` return {fullName} } ``` **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 ( ) } ``` ### 5. Modern React Patterns (React 19+) **Pattern - use() Hook for Promises:** ```typescript // Old pattern: useEffect + loading state function PatientLoader({ patientId }: Props) { const [patient, setPatient] = useState(null) const [loading, setLoading] = useState(true) useEffect(() => { fetchPatient(patientId).then(data => { setPatient(data) setLoading(false) }) }, [patientId]) if (loading) return return } ``` **Modern Solution (React 19):** ```typescript import { use, Suspense } from 'react' // use() unwraps promises and integrates with Suspense function PatientLoader({ patientPromise }: { patientPromise: Promise }) { const patient = use(patientPromise) return } // Parent component function PatientScreen({ patientId }: Props) { const patientPromise = fetchPatient(patientId) return ( }> ) } ``` **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(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 (
{state.error && {state.error}}
) } ``` **Pattern - ref Callback Cleanup (React 19):** ```typescript // Old pattern: useEffect for DOM operations function VideoPlayer({ src }: Props) { const videoRef = useRef