From 7437e2e5840ba9d7a5a42656aa8b7277a75f7357 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 20 May 2026 16:08:19 +0700 Subject: [PATCH] fix: update pattern context --- CLAUDE.md | 146 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 109 insertions(+), 37 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 711d5a1c..a0e24f9b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,76 +80,124 @@ Data tables across all modules (master-data, inventory, finance, purchase, etc.) - Apply to: search handlers, filter form submissions, reset handlers 3. **Create custom formikResetHandler function** - - Clear each filter with `updateFilter(fieldName, defaultValue, true)` - - Call `formik.resetForm({ values: { ...defaults } })` - - Close the modal at the end - - Attach to both button `onClick` and form `onReset` handler + - Call `resetFilter()` (single call — resets all `useTableFilter` state to defaults) + - Reset any local error state (e.g. `setHasDateError(false)`, dismiss toasts) + - Call `formik.resetForm({ values: { ...defaults } })` to sync formik to defaults + - Call `filterModal.closeModal()` at the end + - Attach to form `onReset` handler (not `formik.handleReset`) -**Optimization: Avoid useCallback for simple handlers** + ```tsx + const formikResetHandler = () => { + resetFilter(); + setHasDateError(false); + if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } + formik.resetForm({ values: { start_date: '', end_date: '', customers: [], filterBy: undefined } }); + filterModal.closeModal(); + }; + // ... +
+ ``` -- `useCallback` adds overhead and is only useful for complex logic or memoized child components -- Simple pass-through handlers don't need it: +**Optimization: Avoid useCallback and useMemo for trivial operations** + +- `useCallback` and `useMemo` add overhead; only use them when the computation is expensive or the result is passed to a memoized child +- Simple derivations and pass-through handlers don't need them: ```tsx - // ✅ Good: Simple handler without useCallback - const handleFilterChange = (val) => setFieldValue('location', val); + // ✅ Good: plain derivation + const data = isResponseSuccess(response) ? (response.data ?? []) : []; + const meta = + isResponseSuccess(response) && response.meta ? response.meta : null; - // ❌ Avoid: Unnecessary useCallback overhead - const handleFilterChange = useCallback( + // ❌ Avoid: useMemo for trivial conditional access + const data = useMemo( + () => (isResponseSuccess(response) ? (response.data ?? []) : []), + [response] + ); + + // ✅ Good: simple handler + const handleChange = (val) => setFieldValue('location', val); + + // ❌ Avoid: unnecessary useCallback + const handleChange = useCallback( (val) => setFieldValue('location', val), [setFieldValue] ); ``` +- `useMemo` IS justified for large column definition arrays (TanStack Table re-processes on every render) + **Best practice: Store OptionType objects directly, not IDs** -For select inputs, store the complete `OptionType` object in both formik state and tableFilterState. This eliminates the need for computed helper values (like searching options arrays to find the matching object). +For select inputs, store the complete `OptionType` object (or `OptionType[]` for multi-select) in both formik state and tableFilterState. `useTableFilter`'s `serializeValue` handles serialization automatically: + +- `OptionType` → serialized as `String(value)` in the query string +- `OptionType[]` → serialized as comma-separated values (CSV) — ideal for multi-select API params like `customer_ids`, `sales_ids` ```tsx -// Type the useTableFilter with the filter state structure const { state: tableFilterState, updateFilter, ... } = useTableFilter<{ search: string; - locationFilter?: OptionType; - picFilter?: OptionType; + customers: OptionType[]; // multi-select → serializes as CSV + location?: OptionType; // single-select → serializes as value string + filterBy?: OptionType; // single-select radio }>({ initial: { search: '', - locationFilter: undefined, - picFilter: undefined + customers: [], + location: undefined, + filterBy: undefined, }, paramMap: { page: 'page', pageSize: 'limit', - locationFilter: 'location_id', - picFilter: 'pic_id', + customers: 'customer_ids', // serializes OptionType[] → "1,2,3" + location: 'location_id', // serializes OptionType → "abc" + filterBy: 'filter_by', }, persist: true, - storeName: 'kandangs-table', + storeName: 'my-table', }); -// Initialize formik with tableFilterState values (now typed OptionType objects) -const formik = useFormik({ +// Initialize formik directly from tableFilterState (no hardcoded defaults) +const formik = useFormik({ initialValues: { - location: tableFilterState.locationFilter, - pic: tableFilterState.picFilter, + customers: tableFilterState.customers, + location: tableFilterState.location, + filterBy: tableFilterState.filterBy, }, ... }); -// Handlers store the complete OptionType, not just the ID -const handleFilterLocationChange = useCallback( - (val) => setFieldValue('location', val), - [setFieldValue] -); - -// Use formik values directly in select inputs (no computed helpers needed) - +// Use formik values directly — no computed helpers needed + formik.setFieldValue('customers', Array.isArray(val) ? val : [])} /> + formik.setFieldValue('location', val)} /> + formik.setFieldValue('filterBy', !Array.isArray(val) ? (val ?? undefined) : undefined)} /> ``` +**Filter field naming convention** + +- Multi-select fields: use plural entity name — `customers`, `salesPersons`, `locations` +- Single-select fields: use descriptive camelCase — `filterBy`, `status`, `category` +- No `Filter` suffix (e.g. avoid `customerFilter`, `locationFilter`) + +**Filter modal: pass `openModal` directly, never use `enableReinitialize`** + +`enableReinitialize: true` resets formik mid-interaction whenever `tableFilterState` changes, breaking the modal UX. Pass `filterModal.openModal` directly to the button — no ref wrapper needed. Formik retains its last state across open/close, which is acceptable UX (values sync with `tableFilterState` on submit and reset anyway). + +```tsx +// ❌ Avoid: enableReinitialize breaks modal mid-interaction +const formik = useFormik({ initialValues: { ... }, enableReinitialize: true }); + +// ❌ Avoid: unnecessary ref indirection +const handleFilterModalOpenRef = useRef(() => {}); +handleFilterModalOpenRef.current = () => { formik.setValues({...}); filterModal.openModal(); }; + +// ✅ Correct: pass openModal directly + +``` + +Include `filterModal.openModal` in the `useEffect` deps array when it's used inside the effect. + **Apply this pattern to:** - Any data table component across any module that needs persistent filters @@ -159,7 +207,31 @@ const handleFilterLocationChange = useCallback( **Reference implementations:** - `SupplierTable`, `KandangsTable`, `LocationsTable`, `CustomersTable` in `src/components/pages/master-data/` -- Use same pattern for data tables in other modules (inventory, finance, purchase, etc.) +- `BalanceMonitoringTab` in `src/components/pages/report/finance/tab/` — multi-select + radio + date range + +## SWR fetch pattern + +Use `FinanceApi.getAllFetcher` (or the relevant service's `getAllFetcher`) when the result type matches the service generic `T`. When it differs, use `httpClientFetcher` with an explicit type: + +```tsx +// ✅ Same type as service generic — use getAllFetcher +const { data } = useSWR( + `${Api.basePath}${getTableFilterQueryString()}`, + Api.getAllFetcher +); + +// ✅ Different type — use httpClientFetcher with explicit useSWR type +const { data } = useSWR< + BaseApiResponse, + AxiosError, + SWRHttpKey +>( + `${FinanceApi.basePath}/balance-monitoring${getTableFilterQueryString()}`, + httpClientFetcher +); +``` + +Always name the `toQueryString` alias `getTableFilterQueryString` when destructuring from `useTableFilter`. ## Server-side sorting pattern