Skip to content

Optimize Filter UI (reduce rerendering)#3633

Merged
steven-tey merged 4 commits into
mainfrom
optimize-filter-ui
Mar 23, 2026
Merged

Optimize Filter UI (reduce rerendering)#3633
steven-tey merged 4 commits into
mainfrom
optimize-filter-ui

Conversation

@steven-tey
Copy link
Copy Markdown
Collaborator

@steven-tey steven-tey commented Mar 23, 2026

Summary by CodeRabbit

  • Refactor
    • Filter and search controls were extracted into dedicated, reusable components across multiple dashboard tables for cleaner internal structure while keeping the visible UI and interactions the same.
  • User-facing behavior
    • Empty-state copy now treats any active query parameter (beyond pagination/sorting) as an applied filter, so “no results” messaging better reflects current filters/search.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Mar 23, 2026 6:32pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

Extracted inline filter UI into local helper components across many table pages and removed the isFiltered boolean from several filter hooks; components now derive "filtered" state from router search params (searchParamsObj) instead of hook-provided isFiltered.

Changes

Cohort / File(s) Summary
Payout table (partner & program)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx
Extracted filter UI into local components (PartnerPayoutFilters, PayoutFilters); removed hook isFiltered usage and switched empty-state detection to searchParamsObj.
Payout hooks
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/use-payout-filters.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/use-payout-filters.tsx
Removed computed isFiltered from hook return shape.
Tables with filter UI moved to local components
.../campaigns/campaigns-table.tsx, .../commissions/commissions-table.tsx, .../partners/partners-table.tsx, apps/web/ui/customers/customers-table/customers-table.tsx, apps/web/ui/referrals/partner-referral-table.tsx, .../fraud/fraud-group-table.tsx, .../fraud/resolved/resolved-fraud-group-table.tsx, .../bounties/.../bounty-submissions-table.tsx
In each table, moved inline filter/search UI into a new local component (e.g., CampaignFilters, CommissionsFilters, PartnersFilters, CustomersFilters, PartnerReferralFilters, PendingFraudFilters, ResolvedFraudFilters, BountySubmissionFilters) and adjusted isFiltered computation to use searchParamsObj.
Filter hooks cleanup
.../commissions/use-commission-filters.tsx, .../partners/use-partner-filters.tsx, apps/web/ui/customers/customers-table/use-customer-filters.tsx, apps/web/app/(ee)/partners.dub.co/.../use-payout-filters.tsx
Removed isFiltered from multiple filter hooks' returned objects; preserved other filter data and callbacks.
Router/search adjustments & minor pages
.../groups/groups-table.tsx, .../program/partners/applications/page-client.tsx, .../program/partners/applications/rejected/page-client.tsx
Added searchParamsObj usage and replaced hook isFiltered checks with derived checks on Object.keys(searchParamsObj) (excluding sortBy, sortOrder, page) for empty-state/description logic.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through files, tidy and spry,
Pulled filters out so the table could sigh,
No more isFiltered to puzzle the view,
searchParams hum — the UI feels new,
A carrot for clean code, and off I fly 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main objective of the PR: extracting filter UI components into separate local components to reduce unnecessary re-renders of parent table components when filter state changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch optimize-filter-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
apps/web/ui/referrals/partner-referral-table.tsx (1)

291-291: Use a stable empty object reference to prevent unnecessary memoization recomputes.

The inline {} on line 291 creates a new object reference on every render. Since extraSearchParams is included in the dependency array of useMemo on line 154 of use-program-referral-filters.tsx, this causes the searchQuery memo to recompute unnecessarily each render.

♻️ Suggested fix
+const EMPTY_FILTERS_OPTIONS = {};
+
 function PartnerReferralFilters() {
   const {
     filters,
     activeFilters,
     onSelect,
     onRemove,
     onRemoveAll,
     setSearch,
     setSelectedFilter,
-  } = useProgramReferralsFilters({});
+  } = useProgramReferralsFilters(EMPTY_FILTERS_OPTIONS);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/ui/referrals/partner-referral-table.tsx` at line 291, The inline {}
passed into useProgramReferralsFilters creates a new object each render causing
extraSearchParams to change and trigger recomputations of the searchQuery
useMemo in use-program-referral-filters.tsx; fix by passing a stable reference
instead (e.g., export and use a shared constant like EMPTY_EXTRA_SEARCH_PARAMS
or useRef to hold an immutable empty object) when calling
useProgramReferralsFilters so extraSearchParams remains stable and prevents
unnecessary memo recomputes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/commissions/commissions-table.tsx:
- Around line 383-386: The empty-state message is wrong because the check uses
Object.keys(searchParamsObj).length > 0 and treats sorting params as filters;
update the logic (inside the commissions-table component where searchParamsObj
is used) to ignore sorting keys (e.g., "sortBy" and "sortOrder") when deciding
if filters are active—compute something like filteredKeys =
Object.keys(searchParamsObj).filter(k => !["sortBy","sortOrder"].includes(k))
and use filteredKeys.length > 0 to choose between "No commissions found for the
selected filters." and "No commissions have been made for this program yet."

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/fraud/resolved/resolved-fraud-group-table.tsx:
- Line 33: isFiltered currently treats any URL param as a filter, so when
onSortChange writes sortBy/sortOrder the UI incorrectly shows the filtered empty
state; update the isFiltered computation to ignore sorting params by deriving a
new object or checking keys excluding "sortBy" and "sortOrder" (e.g. compute
filteredSearchParams = Object.keys(searchParamsObj).filter(k => k !== "sortBy"
&& k !== "sortOrder") and use its length) so that isFiltered only reflects true
filters; locate usage around the isFiltered declaration and where onSortChange
sets sortBy/sortOrder to ensure consistency.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/groups/groups-table.tsx:
- Line 91: isFiltered currently uses Object.keys(searchParamsObj).length > 0
which treats sorting/pagination as filters; change the logic in the groups-table
component to ignore sort and pagination params when computing isFiltered by
checking only actual filter keys (e.g., compute filteredKeys =
Object.keys(searchParamsObj).filter(k =>
!["sortBy","sortOrder","page","limit"].includes(k)) and set isFiltered =
filteredKeys.length > 0) or simply use the existing search param check
(!!searchParams.get("search")) if only search is a real filter; update the
isFiltered calculation where searchParamsObj and isFiltered are defined to
reference these symbols.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx:
- Around line 569-572: The message currently treats any searchParamsObj entries
(including sorting params set by onSortChange) as filters; update the isFiltered
check to ignore sorting keys so sorting alone doesn't count as filtered. Locate
the code that computes isFiltered / the conditional using searchParamsObj (and
see onSortChange for which keys it sets) and replace
Object.keys(searchParamsObj).length > 0 with a filtered-key check that excludes
the sort-related param names used by onSortChange (e.g., 'sort', 'order',
'direction' or the exact keys onSortChange writes) and then checks length > 0 on
that filtered array so only true filters trigger the "filtered" message.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx:
- Around line 211-213: The current conditional uses searchParamsObj presence to
decide the empty-state message, which treats any query param (including
pagination/sort) as a filter; change it to detect only actual filter keys by
defining a whitelist (e.g., "status", "startDate", "endDate", "beneficiaryId",
etc.) or a blacklist for non-filter keys ("page", "limit", "sort") and replace
the Object.keys(searchParamsObj).length > 0 check with a utility like
isFilterApplied(searchParamsObj) that returns true only when at least one filter
key is present; update the ternary that renders the message (the expression
using searchParamsObj) to call that utility instead so sorting/pagination params
no longer trigger the "filtered" message.

In `@apps/web/ui/customers/customers-table/customers-table.tsx`:
- Line 76: isFiltered currently treats any URL param (including sortBy/sortOrder
set by onSortChange) as a filter; change the check so it ignores sorting params.
Update the computation around isFiltered in customers-table.tsx to derive it
from Object.keys(searchParamsObj).filter(k => k !== 'sortBy' && k !==
'sortOrder').length > 0 (or equivalently remove those keys before counting) so
clicking a column header doesn't trigger the "filtered" empty-state; reference
the existing searchParamsObj, isFiltered and the onSortChange behavior that
writes sortBy/sortOrder.

---

Nitpick comments:
In `@apps/web/ui/referrals/partner-referral-table.tsx`:
- Line 291: The inline {} passed into useProgramReferralsFilters creates a new
object each render causing extraSearchParams to change and trigger
recomputations of the searchQuery useMemo in use-program-referral-filters.tsx;
fix by passing a stable reference instead (e.g., export and use a shared
constant like EMPTY_EXTRA_SEARCH_PARAMS or useRef to hold an immutable empty
object) when calling useProgramReferralsFilters so extraSearchParams remains
stable and prevents unnecessary memo recomputes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 051e3930-19e4-4875-96e9-043f16b32a6d

📥 Commits

Reviewing files that changed from the base of the PR and between 284a858 and 93ac5d0.

📒 Files selected for processing (16)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/use-payout-filters.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submissions-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commissions-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/use-commission-filters.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-group-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/resolved/resolved-fraud-group-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/groups-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/use-partner-filters.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/use-payout-filters.tsx
  • apps/web/ui/customers/customers-table/customers-table.tsx
  • apps/web/ui/customers/customers-table/use-customer-filters.tsx
  • apps/web/ui/referrals/partner-referral-table.tsx
💤 Files with no reviewable changes (5)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/use-payout-filters.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/use-commission-filters.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/use-partner-filters.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/use-payout-filters.tsx
  • apps/web/ui/customers/customers-table/use-customer-filters.tsx

Comment thread apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/groups-table.tsx Outdated
Comment thread apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx Outdated
Comment thread apps/web/ui/customers/customers-table/customers-table.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/partners/applications/page-client.tsx:
- Line 490: The empty-state message currently treats any query params as
"selected filters" by using Object.keys(searchParamsObj).length > 0; change this
to detect only actual filter/search signals — e.g., check the explicit filter
keys you use (such as status, stage, applicationType, query/search term,
dateRange) or add a helper like isAnyFilterApplied(searchParamsObj, search) that
returns true only if known filter keys are present or search is non-empty; then
replace the condition in the description template (the expression using
searchParamsObj and search) to call that helper or test those specific keys so
the copy correctly shows "for the selected filters" only when real
filters/search are active.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8eda3d41-027c-436a-9fd8-a290aca15fd1

📥 Commits

Reviewing files that changed from the base of the PR and between 93ac5d0 and d9621d3.

📒 Files selected for processing (1)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/page-client.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/rejected/page-client.tsx (1)

123-125: ⚠️ Potential issue | 🟡 Minor

Exclude partnerId from isFiltered here too.

This page uses partnerId for the details sheet, so counting every non-sort query param as a filter can still produce the wrong empty-state copy when no real filter/search is active.

Suggested fix
 const isFiltered = Object.keys(searchParamsObj).some(
-  (key) => !["sortBy", "sortOrder", "page"].includes(key),
+  (key) => !["sortBy", "sortOrder", "page", "partnerId"].includes(key),
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/partners/applications/rejected/page-client.tsx
around lines 123 - 125, The isFiltered calculation currently treats any query
param other than "sortBy", "sortOrder", and "page" as a filter; update the logic
that computes isFiltered (the Object.keys(searchParamsObj).some(...) call) to
also exclude "partnerId" so that partnerId does not count as an active
filter—i.e., add "partnerId" to the list of keys ignored when determining
whether a real filter/search is present.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/page-client.tsx (1)

71-73: ⚠️ Potential issue | 🟡 Minor

Exclude partnerId from isFiltered.

partnerId drives the application sheet, not filtering. If it lingers in the URL, the empty state flips to “selected filters” even when no actual search/filter is active.

Suggested fix
 const isFiltered = Object.keys(searchParamsObj).some(
-  (key) => !["sortBy", "sortOrder", "page"].includes(key),
+  (key) => !["sortBy", "sortOrder", "page", "partnerId"].includes(key),
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/partners/applications/page-client.tsx
around lines 71 - 73, The isFiltered check is incorrectly treating partnerId as
a filter; update the predicate that computes isFiltered (where searchParamsObj
is iterated and key is tested) to also ignore "partnerId" — i.e., add
"partnerId" to the list of keys excluded from filtering (the same place that
currently excludes "sortBy", "sortOrder", and "page" in the isFiltered
computation).
🧹 Nitpick comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (1)

589-633: PartnersFilters still rerenders with every PartnersTable commit.

Pulling the filter UI into a plain child component is mostly an organization change; selection/pagination updates in PartnersTable will still rerender this component and rerun usePartnerFilters. If this PR is meant to reduce filter rerenders, please confirm with the React Profiler and wrap this helper in memo.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
around lines 589 - 633, PartnersFilters is still rerendering on every
PartnersTable commit because it's a plain function component that re-runs
usePartnerFilters; wrap it with React.memo so React can skip rerenders when its
props (sortBy, sortOrder, status) are unchanged and ensure you import memo from
'react' (or export default memo(PartnersFilters)); reference the component name
PartnersFilters and the custom hook usePartnerFilters when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx:
- Around line 47-50: The current isFiltered calculation treats any
non-pagination/sort query key as an active filter; update the logic in the
campaigns-table component (where isFiltered is computed using useRouterStuff and
searchParamsObj) to only consider known filter keys and only count them when
their values are non-empty (trimmed string or non-empty array/object).
Concretely, replace the broad Object.keys check with a whitelist of filter keys
(e.g., name, status, owner, dateFrom, dateTo, etc.) and verify each
searchParamsObj[key] has a meaningful value before returning true so unrelated
or empty query params no longer trigger the filtered state.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/fraud/resolved/resolved-fraud-group-table.tsx:
- Around line 33-35: isFiltered currently treats any searchParamsObj key other
than "sortBy", "sortOrder", and "page" as a filter; exclude the details-sheet
URL state "groupId" from that detection so a URL with only groupId doesn't mark
the table as filtered. Update the isFiltered check (referencing the isFiltered
constant and searchParamsObj) to also ignore "groupId" (e.g., add "groupId" to
the array of non-filter keys or explicitly filter it out before the .some check)
so that only actual filters affect the empty-state description.

---

Duplicate comments:
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/partners/applications/page-client.tsx:
- Around line 71-73: The isFiltered check is incorrectly treating partnerId as a
filter; update the predicate that computes isFiltered (where searchParamsObj is
iterated and key is tested) to also ignore "partnerId" — i.e., add "partnerId"
to the list of keys excluded from filtering (the same place that currently
excludes "sortBy", "sortOrder", and "page" in the isFiltered computation).

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/partners/applications/rejected/page-client.tsx:
- Around line 123-125: The isFiltered calculation currently treats any query
param other than "sortBy", "sortOrder", and "page" as a filter; update the logic
that computes isFiltered (the Object.keys(searchParamsObj).some(...) call) to
also exclude "partnerId" so that partnerId does not count as an active
filter—i.e., add "partnerId" to the list of keys ignored when determining
whether a real filter/search is present.

---

Nitpick comments:
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx:
- Around line 589-633: PartnersFilters is still rerendering on every
PartnersTable commit because it's a plain function component that re-runs
usePartnerFilters; wrap it with React.memo so React can skip rerenders when its
props (sortBy, sortOrder, status) are unchanged and ensure you import memo from
'react' (or export default memo(PartnersFilters)); reference the component name
PartnersFilters and the custom hook usePartnerFilters when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 03fd3d8f-970f-4ba0-b5cd-4fc96d5b9ab0

📥 Commits

Reviewing files that changed from the base of the PR and between d9621d3 and 0c9ef93.

📒 Files selected for processing (9)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commissions-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/resolved/resolved-fraud-group-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/groups-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/rejected/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx
  • apps/web/ui/customers/customers-table/customers-table.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/groups-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commissions-table.tsx
  • apps/web/ui/customers/customers-table/customers-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx

@steven-tey steven-tey merged commit 531baf0 into main Mar 23, 2026
11 checks passed
@steven-tey steven-tey deleted the optimize-filter-ui branch March 23, 2026 18:40
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.

1 participant