Skip to content

Commit

Permalink
Adds query.debounced and updated queries that require debouncing (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
rithviknishad authored Jan 10, 2025
1 parent cf7dcc9 commit e2bf7bb
Show file tree
Hide file tree
Showing 17 changed files with 159 additions and 102 deletions.
15 changes: 15 additions & 0 deletions public/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -328,12 +328,14 @@
"add_new_patient": "Add New Patient",
"add_new_user": "Add New User",
"add_notes": "Add notes",
"add_organizations": "Add Organizations",
"add_patient_updates": "Add Patient Updates",
"add_policy": "Add Insurance Policy",
"add_prescription_medication": "Add Prescription Medication",
"add_prescription_to_consultation_note": "Add a new prescription to this consultation.",
"add_preset": "Add preset",
"add_prn_prescription": "Add PRN Prescription",
"add_questionnaire": "Add Questionnaire",
"add_remarks": "Add remarks",
"add_skill": "Add Skill",
"add_spoke": "Add Spoke Facility",
Expand Down Expand Up @@ -556,6 +558,7 @@
"clear_home_facility": "Clear Home Facility",
"clear_home_facility_confirm": "Are you sure you want to clear the home facility",
"clear_home_facility_error": "Error while clearing home facility. Try again later.",
"clear_search": "Clear search",
"clear_selection": "Clear selection",
"clear_skill": "Clear Skill",
"close": "Close",
Expand Down Expand Up @@ -1187,6 +1190,8 @@
"lsg": "Lsg",
"make_multiple_beds_label": "Do you want to make multiple beds?",
"manage_bed_presets": "Manage Presets of Bed",
"manage_organizations": "Manage Organizations",
"manage_organizations_description": "Add or remove organizations from this questionnaire",
"manage_prescriptions": "Manage Prescriptions",
"manage_preset": "Manage preset {{ name }}",
"manage_user": "Manage User",
Expand Down Expand Up @@ -1288,6 +1293,9 @@
"no_notices_for_you": "No notices for you.",
"no_observations": "No Observations",
"no_ongoing_medications": "No Ongoing Medications",
"no_organizations_found": "No organizations found",
"no_organizations_found_matching": "No organizations found matching {{searchQuery}}",
"no_organizations_selected": "No organizations selected",
"no_patient_record_found": "No Patient Records Found",
"no_patient_record_text": "No existing records found with this phone number. Would you like to register a new patient?",
"no_patients": "No patients found",
Expand All @@ -1300,6 +1308,7 @@
"no_policy_found": "No Insurance Policy Found for this Patient",
"no_presets": "No Presets",
"no_questionnaire_responses": "No Questionnaire Responses",
"no_questionnaires_found": "No questionnaires found",
"no_reason_provided": "No reason provided",
"no_records_found": "No Records Found",
"no_remarks": "No remarks",
Expand All @@ -1312,6 +1321,7 @@
"no_slots_found": "No slots found",
"no_social_profile_details_available": "No Social Profile Details Available",
"no_staff": "No staff found",
"no_sub_organizations_found": "No sub-organizations found",
"no_tests_taken": "No tests taken",
"no_treating_physicians_available": "This facility does not have any home facility doctors. Please contact your admin.",
"no_update_available": "No update available",
Expand Down Expand Up @@ -1518,6 +1528,7 @@
"principal_diagnosis": "Principal diagnosis",
"print": "Print",
"print_referral_letter": "Print Referral Letter",
"priority": "Priority",
"prn_prescription": "PRN Prescription",
"prn_prescriptions": "PRN Prescriptions",
"procedure_suggestions": "Procedure Suggestions",
Expand Down Expand Up @@ -1644,6 +1655,7 @@
"save": "Save",
"save_and_continue": "Save and Continue",
"save_investigation": "Save Investigation",
"saving": "Saving...",
"scan_asset_qr": "Scan Asset QR!",
"schedule": "Schedule",
"schedule_appointment": "Schedule Appointment",
Expand All @@ -1660,6 +1672,7 @@
"search_by_patient_no": "Search by Patient Number",
"search_by_phone_number": "Search by Phone Number",
"search_by_username": "Search by username",
"search_encounters": "Search Encounters",
"search_for_facility": "Search for Facility",
"search_icd11_placeholder": "Search for ICD-11 Diagnoses",
"search_investigation_placeholder": "Search Investigation & Groups",
Expand Down Expand Up @@ -1696,7 +1709,9 @@
"select_skills": "Select and add some skills",
"select_time": "Select time",
"select_time_slot": "Select time slot",
"select_user": "Select user",
"select_wards": "Select wards",
"selected_organizations": "Selected Organizations",
"selected_slot_not_found": "Selected Slot Not Found",
"self_booked": "Self-booked",
"send": "Send",
Expand Down
29 changes: 29 additions & 0 deletions src/Utils/request/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,35 @@ useQuery({
});
```

### Debounced Queries

For search inputs or other scenarios requiring debounced API calls, use `query.debounced`:

```tsx
function SearchComponent() {
const [search, setSearch] = useState("");
const { data } = useQuery({
queryKey: ['search', search],
queryFn: query.debounced(routes.search, {
queryParams: { q: search },
debounceInterval: 500 // Optional: defaults to 500ms
}),
enabled: search.length > 0
});
return (
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
);
}
```

The debounced query will wait for the specified interval after the last call before executing the request, helping to reduce unnecessary API calls during rapid user input.

### Error Handling

All API errors are now handled globally. Common scenarios like:
Expand Down
41 changes: 41 additions & 0 deletions src/Utils/request/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import careConfig from "@careConfig";
import { getResponseBody } from "@/Utils/request/request";
import { APICallOptions, HTTPError, Route } from "@/Utils/request/types";
import { makeHeaders, makeUrl } from "@/Utils/request/utils";
import { sleep } from "@/Utils/utils";

export async function callApi<TData, TBody>(
{ path, method, noAuth }: Route<TData, TBody>,
Expand Down Expand Up @@ -67,3 +68,43 @@ export default function query<TData, TBody>(
return callApi(route, { ...options, signal });
};
}

/**
* Creates a debounced TanStack Query compatible query function.
*
* Example:
* ```tsx
* const { data, isLoading } = useQuery({
* queryKey: ["patient-search", facilityId, search],
* queryFn: query.debounced(patientsApi.search, {
* pathParams: { facilityId },
* queryParams: { limit: 10, offset: 0, search },
* }),
* });
* ```
*
* The debounced query leverages TanStack Query's built-in cancellation through
* `AbortSignal`. Here's how it works:
*
* 1. When a new query is triggered, TanStack Query automatically creates an
* `AbortSignal`
* 2. If a new query starts before the debounce delay finishes:
* - The previous signal is aborted automatically by TanStack Query
* - The previous `sleep` promise is cancelled
* - A new debounce timer starts
*
* No explicit cleanup is needed because:
* - The `AbortSignal` is passed through to the underlying `fetch` call
* - When aborted, both the `sleep` promise and the fetch request are cancelled automatically
* - TanStack Query handles the abortion and cleanup of previous in-flight requests
*/
const debouncedQuery = <TData, TBody>(
route: Route<TData, TBody>,
options?: APICallOptions<TBody> & { debounceInterval?: number },
) => {
return async ({ signal }: { signal: AbortSignal }) => {
await sleep(options?.debounceInterval ?? 500);
return query(route, { ...options })({ signal });
};
};
query.debounced = debouncedQuery;
12 changes: 4 additions & 8 deletions src/components/Common/SearchByMultipleFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,10 @@ const SearchByMultipleFields: React.FC<SearchByMultipleFieldsProps> = ({
}, [selectedOptionIndex]);

useEffect(() => {
const timeout = setTimeout(
() =>
selectedOption.value !== searchValue &&
onSearch(selectedOption.key, searchValue),
1000,
);
return () => clearTimeout(timeout);
}, [searchValue]);
if (selectedOption.value !== searchValue) {
onSearch(selectedOption.key, searchValue);
}
}, [searchValue, selectedOption.key, selectedOption.value, onSearch]);

const handleSearchChange = useCallback((event: EventType) => {
const value = "target" in event ? event.target.value : event.value;
Expand Down
6 changes: 2 additions & 4 deletions src/components/Common/UserSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import {

import { Avatar } from "@/components/Common/Avatar";

import useDebouncedState from "@/hooks/useDebouncedState";

import query from "@/Utils/request/query";
import { formatName } from "@/Utils/utils";
import { UserBase } from "@/types/user/user";
Expand All @@ -45,11 +43,11 @@ export default function UserSelector({
}: Props) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [search, setSearch] = useDebouncedState("", 500);
const [search, setSearch] = useState("");

const { data, isFetching } = useQuery({
queryKey: ["users", search],
queryFn: query(UserApi.list, {
queryFn: query.debounced(UserApi.list, {
queryParams: { search_text: search },
}),
});
Expand Down
44 changes: 12 additions & 32 deletions src/components/Patient/PatientIndex.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMutation } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { navigate } from "raviger";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import useKeyboardShortcut from "use-keyboard-shortcut";
Expand Down Expand Up @@ -34,25 +34,16 @@ import SearchByMultipleFields from "@/components/Common/SearchByMultipleFields";
import { GENDER_TYPES } from "@/common/constants";

import routes from "@/Utils/request/api";
import mutate from "@/Utils/request/mutate";
import query from "@/Utils/request/query";
import { parsePhoneNumber } from "@/Utils/utils";
import { PartialPatientModel } from "@/types/emr/newPatient";

interface PatientListResponse {
results: PartialPatientModel[];
count: number;
}

export default function PatientIndex({ facilityId }: { facilityId: string }) {
const [phoneNumber, setPhoneNumber] = useState("");
const [yearOfBirth, setYearOfBirth] = useState("");
const [selectedPatient, setSelectedPatient] =
useState<PartialPatientModel | null>(null);
const [verificationOpen, setVerificationOpen] = useState(false);
const [patientList, setPatientList] = useState<PatientListResponse>({
results: [],
count: 0,
});
const { t } = useTranslation();

const handleCreatePatient = useCallback(() => {
Expand Down Expand Up @@ -104,19 +95,14 @@ export default function PatientIndex({ facilityId }: { facilityId: string }) {
}
}, []);

const { mutate: listPatients, isPending } = useMutation({
mutationFn: () =>
mutate(routes.searchPatient, {
body: {
phone_number: parsePhoneNumber(phoneNumber) || "",
},
})({ phone_number: parsePhoneNumber(phoneNumber) || "" }),
onSuccess: (data) => {
setPatientList({
results: data.results,
count: data.count,
});
},
const { data: patientList, isFetching } = useQuery({
queryKey: ["patient-search", facilityId, phoneNumber],
queryFn: query.debounced(routes.searchPatient, {
body: {
phone_number: parsePhoneNumber(phoneNumber) || "",
},
}),
enabled: !!phoneNumber,
});

const handlePatientSelect = (patient: PartialPatientModel) => {
Expand All @@ -140,12 +126,6 @@ export default function PatientIndex({ facilityId }: { facilityId: string }) {
});
};

useEffect(() => {
if (phoneNumber) {
listPatients();
}
}, [phoneNumber, listPatients]);

return (
<div>
<div className="container max-w-5xl mx-auto py-6">
Expand Down Expand Up @@ -174,7 +154,7 @@ export default function PatientIndex({ facilityId }: { facilityId: string }) {
<div className="min-h-[200px]" id="patient-search-results">
{!!phoneNumber && (
<>
{isPending ? (
{isFetching || !patientList ? (
<div className="flex items-center justify-center h-[200px]">
<Loading />
</div>
Expand Down
Loading

0 comments on commit e2bf7bb

Please sign in to comment.