Loading states
-
When creating custom hooks that wrap SWR, memoize the return value using
useMemoto prevent unnecessary re-renders. This is especially important when returning an object with multiple properties:import { restBaseUrl, useOpenmrsSWR } from '@openmrs/esm-framework'; // Good - memoized return value export function usePatient(patientUuid?: string) { const url = patientUuid ? `${restBaseUrl}/patient/${patientUuid}` : null; const { data: response, error, isLoading, isValidating } = useOpenmrsSWR<Patient>(url); const patient = response?.data; return useMemo( () => ({ isLoading, isValidating, patient, patientUuid, error, }), [isLoading, isValidating, error, patient, patientUuid], ); } -
Use SWR’s
isLoadingfor the initial blocking load, and useisValidatingwhen you need to show a background refresh indicator while existing data remains on screen. Avoid treating every revalidation as a full loading state when stale data is already available:// Good - initial load and background refresh are distinct const { data: response, error, isLoading, isValidating } = useOpenmrsSWR<Patient>(url); const patient = response?.data; if (isLoading) { return <SkeletonPlaceholder />; } if (error) { return <ErrorState error={error} />; } if (!patient) { return <EmptyState />; } return ( <> {isValidating ? <InlineLoading description={t('refreshing', 'Refreshing')} /> : null} <PatientBanner patient={patient} /> </> ); -
Use Carbon’s loading components to provide visual feedback during data fetching:
- Use
InlineLoadingfor inline loading indicators within forms or small components - Use
SkeletonTextorSkeletonPlaceholderfor loading placeholders that match the final content layout
import { InlineLoading, SkeletonText } from '@carbon/react'; // Inline loading indicator {isLoading && ( <InlineLoading description={t('loading', 'Loading')} /> )} // Skeleton placeholder for content {isLoading ? ( <SkeletonText heading width="60%" /> ) : ( <div>{data?.content}</div> )} - Use
-
Handle null or undefined data gracefully using optional chaining and nullish coalescing:
// Good - safe data access const patientName = data?.patient?.name ?? t('unknownPatient', 'Unknown patient'); const totalCount = data?.total ?? 0; // Good - conditional rendering {data?.results?.length > 0 ? ( <DataTable data={data.results} /> ) : ( <EmptyState /> )} -
When data might be undefined during initial load, use conditional rendering to avoid errors:
// Good - handles undefined data {data && ( <PatientBanner patient={data} /> )} // Also good - using optional chaining {data?.patient && ( <PatientBanner patient={data.patient} /> )} -
Use TypeScript type guards in filter functions to ensure type safety when filtering out invalid data:
// Good - type guard ensures filtered array has correct type const validPatients = data?.filter( (patient): patient is Patient => patient !== null && patient.person !== null ) ?? []; // TypeScript now knows validPatients is Patient[], not (Patient | null)[] validPatients.forEach(patient => { console.log(patient.person.name); // Type-safe access });