Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 8, 2026

Proposed Changes

Multiple components created new AbortController().signal without storing the controller reference, preventing cleanup. This caused memory leaks (requests continuing after unmount), race conditions (stale requests updating state), and React warnings.

useEffect fixes (critical):

  • MedicationBillForm.tsx, DispenseDrawer.tsx: Store AbortController, add cleanup function, check signal.aborted before setState
  • Wrap API calls in try-catch to ignore AbortError

TanStack Query fixes:

  • NoteManager.tsx: Use destructured signal from queryFn context instead of creating new controller
  • AccountSheet.tsx, TagConfigView.tsx, DispensedMedicationList.tsx: Add required signal parameter to query calls in mutations

Event handler fix:

  • SpecimenIDScanDialog.tsx: Use useRef to persist AbortController, cleanup on unmount, abort previous requests

Before:

useEffect(() => {
  const fetchData = async () => {
    const response = await query(api)({ 
      signal: new AbortController().signal // Can't be cancelled
    });
    setState(response.data); // May run after unmount
  };
  fetchData();
}, [deps]);

After:

useEffect(() => {
  const abortController = new AbortController();
  
  const fetchData = async () => {
    try {
      const response = await query(api)({ signal: abortController.signal });
      if (!abortController.signal.aborted) {
        setState(response.data);
      }
    } catch (error) {
      if (error instanceof Error && error.name !== "AbortError") {
        // Handle real errors
      }
    }
  };
  
  fetchData();
  return () => abortController.abort();
}, [deps]);

Tagging: @ohcnetwork/care-fe-code-reviewers

Merge Checklist

  • Add specs that demonstrate the bug or test the new feature.
  • Update product documentation.
  • Ensure that UI text is placed in I18n files.
  • Prepare a screenshot or demo video for the changelog entry and attach it to the issue.
  • Request peer reviews.
  • Complete QA on mobile devices.
  • Complete QA on desktop devices.
  • Add or update Playwright tests for related changes
Original prompt

This section details on the original issue you should resolve

<issue_title>Missing AbortController Cleanup in useEffect Hooks Causing Memory Leaks and Race Conditions</issue_title>
<issue_description>Describe the bug

Several components are creating new AbortController().signal instances in useEffect hooks without storing the controller reference or implementing proper cleanup. This leads to:

  1. Memory leaks: API requests continue running even after components unmount, consuming network resources and browser memory
  2. Race conditions: Stale requests can complete and update component state after unmount or when dependencies change, potentially overwriting fresh data with outdated information
  3. React warnings: Attempts to call setState on unmounted components trigger React warnings in development
  4. Unnecessary network traffic: Requests cannot be cancelled when they should be, wasting bandwidth and server resources

This issue affects critical healthcare workflows including pharmacy medication billing, inventory management, and clinical data entry forms.

To Reproduce

Steps to reproduce the behavior:

  1. Open browser DevTools (Network tab and Console)
  2. Navigate to a medication billing page (e.g., /facility/{id}/services/pharmacy)
  3. Interact with the page to trigger inventory fetching (e.g., select medications or change filters)
  4. Quickly navigate away from the page or change filters that trigger dependency updates
  5. Observe in the Network tab that API requests continue running after navigation
  6. Check the Console for React warnings about "Can't perform a React state update on an unmounted component"
  7. Repeat steps 2-6 multiple times to observe memory accumulation

Expected behavior

When a component unmounts or its dependencies change, all in-flight API requests should be cancelled immediately. The AbortController should be stored in a variable, used for the request signal, and properly aborted in the useEffect cleanup function. This prevents memory leaks, eliminates race conditions, and ensures efficient resource usage.

Screenshots

Not applicable - this is a code-level issue affecting request cancellation behavior. The Network tab will show requests continuing after component unmount, and the Console will display React warnings in development mode.

Desktop :

  • OS: macOS
  • Browser: Chrome
  • Version: Latest

Smartphone:

  • Device: N/A
  • OS: N/A
  • Browser: N/A
  • Version: N/A

Additional context

This issue affects 7+ files across the codebase, with the most critical being:

  • src/pages/Facility/services/pharmacy/MedicationBillForm.tsx (line 826)
  • src/components/Consumable/DispenseDrawer.tsx (line 197)
  • src/pages/Facility/billing/account/AccountSheet.tsx (line 120)
  • src/pages/Facility/services/pharmacy/DispensedMedicationList.tsx (line 448)
  • src/components/Scan/SpecimenIDScanDialog.tsx (line 46)
  • src/pages/Admin/TagConfig/TagConfigView.tsx (line 74)
  • Multiple Questionnaire question components

Current problematic pattern:

useEffect(() => {
  const fetchData = async () => {
    const response = await query(api.endpoint, {
      // ... options
    })({ signal: new AbortController().signal }); // ❌ Controller is lost, can't be cancelled
    
    setState(response.data);
  };
  
  fetchData();
}, [dependencies]);

Correct pattern should be:

useEffect(() => {
  const abortController = new AbortController();
  
  const fetchData = async () => {
    try {
      const response = await query(api.endpoint, {
        // ... options
      })({ signal: abortController.signal });
      
      // Check if request was aborted before setting state
      if (!abortController.signal.aborted) {
        setState(response.data);
      }
    } catch (error) {
      // Handle abort errors gracefully
      if (error instanceof Error && error.name !== 'AbortError') {
        // Handle real errors
      }
    }
  };
  
  fetchData();
  
  // Cleanup: abort request when component unmounts or dependencies change
  return () => {
    abortController.abort();
  };
}, [dependencies]);

Note: This is separate from the TanStack Query signal parameter issue that was recently fixed. These are manual useEffect hooks that need proper cleanup, particularly in cases where complex conditional fetching patterns make TanStack Query refactoring impractical.</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@netlify
Copy link

netlify bot commented Jan 8, 2026

Deploy Preview for care-ohc ready!

Name Link
🔨 Latest commit 74aa41d
🔍 Latest deploy log https://app.netlify.com/projects/care-ohc/deploys/695fa644c5aa0a0008d34cdf
😎 Deploy Preview https://deploy-preview-15061.preview.ohc.network
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 8, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI and others added 2 commits January 8, 2026 12:29
- Add proper AbortController cleanup in MedicationBillForm.tsx useEffect
- Add proper AbortController cleanup in DispenseDrawer.tsx useEffect
- Use TanStack Query signal parameter in NoteManager.tsx useInfiniteQuery
- Remove improper signal in AccountSheet.tsx useMutation
- Remove improper signal in TagConfigView.tsx useMutation
- Add AbortController ref cleanup in SpecimenIDScanDialog.tsx event handler
- Simplify DispensedMedicationList.tsx completeMedications mutation

Co-authored-by: Jacobjeevan <[email protected]>
…n mutations

- AccountSheet: Add signal parameter to query call in updateMutation
- TagConfigView: Add signal parameter to query call in archiveMutation
- DispensedMedicationList: Add signal parameter to query call in completeMedications

Note: These are user-initiated mutations that should complete, so signal won't be aborted

Co-authored-by: Jacobjeevan <[email protected]>
Copilot AI changed the title [WIP] Fix missing AbortController cleanup in useEffect hooks Fix missing AbortController cleanup in useEffect hooks causing memory leaks Jan 8, 2026
Copilot AI requested a review from Jacobjeevan January 8, 2026 12:45
Comment on lines +188 to +202
try {
const promises = missingInventories.map(
async ([productKnowledgeId]) => {
const inventoriesResponse = await query(inventoryApi.list, {
pathParams: { facilityId, locationId: currentLocation.id },
queryParams: {
limit: 100,
product_knowledge: productKnowledgeId,
net_content_gt: 0,
},
})({ signal: abortController.signal });

return {
productKnowledgeId,
inventories: inventoriesResponse.results || [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we doing query manually in a useEffect when we have tanstack query?

we can simply use useQueries instead

signal,
body: { datapoints: updates },
})({ signal });
})({ signal: new AbortController().signal });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the point when new abort controller signals are made each time mutate is called?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing AbortController Cleanup in useEffect Hooks Causing Memory Leaks and Race Conditions

3 participants