-
Notifications
You must be signed in to change notification settings - Fork 4
fix: prevent double loading during PayrollConfiguration pagination #928
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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) |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
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.
| const [syncedEmployeeData, setSyncedEmployeeData] = useState(employeeData) | |
| const [syncedEmployeeData, setSyncedEmployeeData] = | |
| useState<typeof employeeData | undefined>(undefined) |
| } | ||
| }, [isDataInSync, employeeData]) | ||
|
|
||
| const displayedEmployees = syncedEmployeeData?.showEmployees || [] |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
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.
| const displayedEmployees = syncedEmployeeData?.showEmployees || [] | |
| const displayedEmployees = syncedEmployeeData?.showEmployees ?? [] |
63afefe to
fa1f6d7
Compare
| if (!employeeData) { | ||
| return <Loading /> | ||
| } |
There was a problem hiding this comment.
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?
| useEffect(() => { | ||
| if (isDataInSync && employeeData) { | ||
| setSyncedEmployeeData(employeeData) | ||
| } | ||
| }, [isDataInSync, employeeData]) |
There was a problem hiding this comment.
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
| 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]) |
There was a problem hiding this comment.
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.
2e23cca to
6b58d24
Compare
serikjensen
left a comment
There was a problem hiding this 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.
Problem
When paginating in PayrollConfiguration, users see two loading states instead of one:
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
useEmployeesListSuspensetouseEmployeesListwithkeepPreviousDataisDataInSyncto detect when prepared payroll matches current employeessyncedEmployeeDatastate that only updates when both datasets syncdisplayedEmployeesfor rendering to prevent mismatched data flashResult
isDataInSync = falseisDataInSync = trueProof 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