Articles
Data Table Coding Interview: Sorting, Selection, and Server Pagination
Practice data table interview design with server sorting, stable row selection, Select all semantics, pagination resets, and race-safe requests.
A data table often looks finished when its first page renders. The difficult part begins when the user sorts a column, checks a row, changes pages, and receives responses out of order. A strong coding-interview solution treats the table as a state contract: what is controlled by the client, what is computed by the server, which row identity survives pagination, and how stale requests are ignored.
The TanStack Table guide on client-side versus server-side processing and its sorting, pagination, and row-selection guides provide official library behavior. The state trace below is an original interview exercise, not an employer-specific assignment. The same principles apply even if the interviewer asks for a hand-built table rather than TanStack Table.
Clarify the table contract before writing components
Ask what the API returns: all rows or one page, a total count or an opaque next-page cursor, stable row IDs, and supported sort keys. Ask whether filtering and sorting happen on the client or server. Ask what “Select all” means: only visible rows, all loaded rows, or every row matching the current query. These are different features with different data requirements. Also ask whether selection persists when the filter changes and whether the URL should preserve sort and page state.
Define the smallest state shape that reflects the answers. For a server-paginated table, it might contain {pageIndex, pageSize, sorting, filters, selectedIds} plus loading/error state and a request generation. selectedIds is keyed by stable server IDs, not page offsets. The rows returned by the server are a response to a query state, not the entire dataset. This distinction prevents a class of bugs in which the UI silently applies a local sort to only one page.
Make the server contract explicit. For example, GET /items?page=2&size=20&sort=name:asc should return rows for precisely that query, plus enough pagination metadata to know whether a next page exists. Decide whether page indexes are zero- or one-based at the API boundary and convert once. If filters or sorting change, reset the page to the first valid page unless the product has another rule. Otherwise a user can be left on page seven of a filtered result that has only two pages.
Why does sorting only the current page fail?
Suppose the server returns page 1 with IDs 11, 18, and 42, while a row on page 2 has a name alphabetically before all three. Sorting the three loaded rows on the client produces a neat-looking current page, but it does not produce the first page of the globally sorted dataset. The visual lies about ranking. The TanStack sorting guide explicitly distinguishes client-side and manual server-side sorting. With manual sorting, the table assumes the supplied data has already been sorted as intended.
The same principle applies to pagination. Manual pagination tells the table that the data it receives is already a page. A component should not pretend it has a complete local row model unless it actually does. If all rows are loaded and the dataset is small, client-side sorting and pagination can be simpler. The interview answer should be driven by scale, API behavior, and consistency requirements, not by a reflex to use the most complicated mode.
A useful trace is: the user clicks Name ascending; the client updates sorting, resets pageIndex, sends a query with those parameters, and renders only the response for that current query. If the backend does not support Name sorting, either negotiate the feature or state the limitation. Sorting a single server page locally is not a substitute for global ordering.
Trace selection across pages with stable identity
Here is an original state trace. The user is on page 1, sees row ID 42, and checks it. selectedIds becomes {42}. The user moves to page 2, which does not contain 42. The checkmark is not visible, but selectedIds still contains 42. If the user returns to page 1, row 42 should be checked again. Its identity is the server ID; its page position is irrelevant.
| Event | Query state | Visible IDs | Selected IDs | Required behavior |
|---|---|---|---|---|
| Initial page | Page 1, Name ascending | 11, 18, 42 | ∅ | No checkmarks. |
| Check row 42 | Same query | 11, 18, 42 | {42} | Row 42 checked. |
| Move to page 2 | Page 2, Name ascending | 5, 27, 91 | {42} | Selection retained off-page. |
| Check row 27 | Same query | 5, 27, 91 | {42, 27} | Two selected IDs. |
| Change sort to newest | Page resets to 1 | New response | {42, 27} or ∅ | Follow the stated product rule. |
The final row is deliberately a product decision. Some tools preserve explicit selected IDs across sorts and filters; others clear selection when the result set changes. Say which rule you chose and test it. A table cannot safely infer “selected row count” from only the currently visible page when selection spans pages.
TanStack’s row-selection guide notes that row-selection state can hold IDs not present in the current page, while a selected-row model built from manual-pagination data can only materialize rows on the current page. It also recommends using stable IDs through getRowId rather than default row indexes for durable selection. That gives an interview candidate a precise distinction: the state can remember off-page IDs, but their full row objects may need a separate fetch or action endpoint.
What does “Select all” mean when rows live on the server?
A page checkbox can select all 20 visible IDs because the client has them. Selecting all rows that match a filter is a different operation: the client may never have loaded their IDs. A robust API often represents that intent as the current filter/query plus an exclusion set, or performs a bulk action server-side against a validated query. Sending “select all” as a list of the current page’s IDs and labeling it “all results” is a correctness bug.
Ask how the action should behave if rows change between selection and execution. For destructive or financial operations, the server should define a snapshot or validation rule rather than trusting a stale client count. In a coding interview you may not implement a bulk-action endpoint, but you should mark the boundary and avoid promising a capability your state model cannot fulfill.
Checkbox state also needs three distinct views: none of the current page selected, all of the current page selected, and some selected. An indeterminate checkbox can communicate the third. If selection spans pages, show a separate total selected-ID count only when that count is well-defined. This prevents a “0 selected” label on page 2 while an off-page row remains selected.
How do you keep late responses from replacing newer state?
Suppose page 1 requests Name ascending, then the user quickly changes to newest. Request A starts first but returns last. If the component blindly sets rows on every response, A overwrites the newer response B and the table shows old ordering under a “newest” indicator. This is a race between query state and network completion.
One solution is to cancel obsolete fetches with AbortController. Another is to attach a monotonically increasing request generation or query key and commit a response only if it matches the latest query. Often both are useful: cancellation saves work where supported, while the generation check protects state from requests that cannot be cancelled. Keep loading and error state tied to the same query key, or a late error can replace a newer success.
A compact pseudocode contract is:
on query change:
key = serialize(pageIndex, pageSize, sorting, filters)
mark key loading
request rows for key
when response arrives:
if key is still current: commit rows and count
otherwise: discard response
This is intentionally framework-neutral. In React, implement it with a data-fetching cache keyed on query parameters or a carefully scoped effect. Do not put a mutable “is current” flag in a place that is recreated incorrectly on every render. Explain the race with A and B first; then choose the mechanism your project already supports.
How do URL state and accessibility fit the contract?
If the product expects a shareable view, encode page, sort, and filters in the URL. On reload, parse and validate those values before issuing a request. An unsupported sort key should not be sent straight to the server or silently shown as active. When a filter changes, update the URL and page together so browser Back restores a coherent query rather than an old page number with a new filter. Selection may remain local if sharing it would expose private or transient IDs; that is a separate product decision.
Give each sortable header a clear label and expose its current sort direction. Keep checkbox labels tied to stable row identity, not just a row position that changes after sorting. Loading state should not erase the user's understanding of the current query, and an error message should identify which request can be retried. These details make the state contract observable to a keyboard or screen-reader user as well as to the developer running tests.
What should the reviewer see in a working submission?
A minimal, reviewable version has an accessible table or grid, clear sort controls, loading and error states, a stable empty state, and page controls whose disabled state matches server metadata. Sorting should update the query and reset the page. Selecting a row should use its stable ID. Returning to a page should restore its checkbox if the chosen product rule preserves selection. A stale response should not overwrite the latest query.
Test the boundary with a controllable fake server rather than a happy-path static array. Return page 2 before page 1, reverse the completion order of two sorts, and include a duplicated display name with distinct IDs. Verify selection by ID, not by visible row text. If the API errors, keep the query state understandable and offer a retry for that same query. If total count is unknown, do not invent a final page number.
In review, explain any deliberate scope limit. For example: “The checkbox selects only the current page. Cross-page explicit IDs persist, but selecting every match requires a server bulk-selection contract.” That is stronger than a visually impressive UI with ambiguous semantics. It turns a common coding exercise into a clear product and data-system design.
Practice questions that exercise the whole state contract
| Practice question | Focus |
|---|---|
| Build a Figma-Specified React Data Table | Implement a concrete table UI. |
| Selection-State Class for an Infinite-Scroll Checkbox List with Select All | Define selection beyond visible rows. |
| Implement a React CRUD Table | Coordinate mutations with table state. |
| Clarify and Implement Search Filters for a Library Table | Translate ambiguous filtering requirements. |
| Debug a Frontend Component That Shows Wrong API Results | Diagnose stale or mismatched responses. |
When you can narrate one user action through query state, API request, response validation, visible rows, and selected IDs, you have the core interview answer. A table is reliable when those transitions remain consistent, even on a slow network and after the user changes their mind.