Skip to content

Conversation

@jeffredodd
Copy link
Contributor

@jeffredodd jeffredodd commented Jan 8, 2026

Problem

When paginating in PayrollConfiguration, users see two loading states instead of one:

  1. First load: Employee list data fetches (names, basic info)
  2. Second load: Prepared payroll data fetches (compensation data)

During the transition, the table renders with mismatched data - new employee names with old compensation data - before the second fetch completes.

Solution

Keep showing previous page data until BOTH datasets are synchronized.

Changes

  • Switch from useEmployeesListSuspense to useEmployeesList with keepPreviousData
  • Add isDataInSync to detect when prepared payroll matches current employees
  • Add syncedEmployeeData state that only updates when both datasets sync
  • Use displayedEmployees for rendering to prevent mismatched data flash
  • Combined loading state shows spinner until both fetches complete
  • Add null check for initial load

Result

  1. User clicks next page → spinner starts
  2. New employee data arrives, but isDataInSync = false
  3. Table continues showing previous page (no flash)
  4. Prepared payroll completes → isDataInSync = true
  5. Both datasets swap atomically → single clean render
  6. Spinner stops

Proof of Functionality

Before

Screen.Recording.2026-01-08.at.9.44.58.AM.mov

After

Screen.Recording.2026-01-08.at.8.48.46.AM.mov

Copilot AI review requested due to automatic review settings January 8, 2026 16:51
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a double-loading issue during pagination in PayrollConfiguration where users would see employee names update before their corresponding compensation data, causing a visual data mismatch. The fix ensures both datasets are synchronized before updating the displayed data.

Key changes:

  • Switch from Suspense-based employee data fetching to regular fetching with keepPreviousData
  • Add synchronization logic to keep displaying previous page data until both employee and compensation datasets match
  • Combine loading states to show a single spinner until both fetches complete

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

return employeeUuids.every(uuid => preparedUuids.has(uuid))
}, [preparedPayroll?.employeeCompensations, employeeUuids])

const [syncedEmployeeData, setSyncedEmployeeData] = useState(employeeData)
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

Initial state is set to employeeData which may be undefined on first render since useEmployeesList no longer uses Suspense. This could cause displayedEmployees to be an empty array even when data exists. Initialize with undefined or null instead, and update the useEffect dependency to handle the initial data load.

Suggested change
const [syncedEmployeeData, setSyncedEmployeeData] = useState(employeeData)
const [syncedEmployeeData, setSyncedEmployeeData] =
useState<typeof employeeData | undefined>(undefined)

Copilot uses AI. Check for mistakes.
}
}, [isDataInSync, employeeData])

const displayedEmployees = syncedEmployeeData?.showEmployees || []
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

This fallback to an empty array could mask the difference between 'no data yet' and 'legitimately empty page'. Consider using a more explicit null check pattern that distinguishes between loading states and empty results.

Suggested change
const displayedEmployees = syncedEmployeeData?.showEmployees || []
const displayedEmployees = syncedEmployeeData?.showEmployees ?? []

Copilot uses AI. Check for mistakes.
@jeffredodd jeffredodd marked this pull request as draft January 8, 2026 16:59
@jeffredodd jeffredodd marked this pull request as ready for review January 8, 2026 17:56
@jeffredodd jeffredodd force-pushed the jdj/fix-payroll-config-double-load branch from 63afefe to fa1f6d7 Compare January 8, 2026 17:56
Comment on lines +265 to +280
if (!employeeData) {
return <Loading />
}
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we base this on isFetching instead? could we have no value for employee data here but not actually be loading?

Comment on lines 133 to 137
useEffect(() => {
if (isDataInSync && employeeData) {
setSyncedEmployeeData(employeeData)
}
}, [isDataInSync, employeeData])
Copy link
Member

Choose a reason for hiding this comment

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

Recommendation here to figure out if we can do this via callback for the data fetching rather than running it through an effect, given employeeData is a large object that is compared here in the dep array via referential equality

Comment on lines 123 to 127
const isDataInSync = useMemo(() => {
if (!preparedPayroll?.employeeCompensations || employeeUuids.length === 0) return false
const preparedUuids = new Set(preparedPayroll.employeeCompensations.map(c => c.employeeUuid))
return employeeUuids.every(uuid => preparedUuids.has(uuid))
}, [preparedPayroll?.employeeCompensations, employeeUuids])
Copy link
Member

Choose a reason for hiding this comment

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

I have similar concerns here memoizing this where the employeeUuids array is being compared via referential equality here in the dep array since it's an array of strings. Same with employee compensations. Wondering if we manage to make the change below for the effect if we can just run this when we execute that rather than placing it in a memo

- Switch from useEmployeesListSuspense to useEmployeesList with keepPreviousData
- Add isDataInSync to detect when prepared payroll matches current employees
- Add syncedEmployeeData state that only updates when both datasets sync
- Use displayedEmployees for rendering to prevent mismatched data flash
- Combined loading state shows spinner until both fetches complete
- Add null check for initial load
- Initialize syncedEmployeeData with undefined instead of potentially undefined employeeData
- Use nullish coalescing (??) instead of || for displayedEmployees fallback
… state

- Add isPaginating flag to distinguish initial load from pagination
- Use isPaginating for pagination fetching indicator instead of full isLoading
- Add staleTime: Infinity to paySchedule query (doesn't change during pagination)
- Memoize employeeUuidsKey to prevent unnecessary effect re-runs
- Result: old data stays visible during page changes instead of loading spinner
…onDataReady

Callback is memoized by the caller, so the defensive ref pattern isn't needed.
@jeffredodd jeffredodd force-pushed the jdj/fix-payroll-config-double-load branch from 2e23cca to 6b58d24 Compare January 9, 2026 17:23
Copy link
Member

@serikjensen serikjensen left a comment

Choose a reason for hiding this comment

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

Let's chat about this when you're back in, we've extracted employeeData into a ref, but i'm unsure if that actually resolves the concerns i raised.

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.

3 participants