Generalise the table into one reusable component. The previous tables hard-coded the columns and the data; here you pass both in as props — a columns config and a data array — so the same <DataTable> works for users, products, or anything else. It folds together the two earlier features: column-driven sorting (Data Table II) and pagination (Data Table), in that order.
type Column = { key: string; header: string; sortable?: boolean };
type Row = { id: number; [key: string]: string | number };
function DataTable(props: {
columns: Column[];
data: Row[];
pageSize: number;
}): JSX.Element;
<DataTable columns={userCols} data={users} pageSize={5} />
<DataTable columns={productCols} data={products} pageSize={10} />
→ same component, different config, no code changes
sort by Age, then page → page 2 holds the next-oldest five,
NOT a re-sort of page 2's rows in isolation (sort the whole set first)
columns; cells read row[col.key]. No field names baked in.The leap here is from a table that knows about users to a table that knows about nothing — it takes a columns config and a data array and renders whatever you give it. Inside, it runs the two transforms you've already built: sort the whole dataset, then slice out the current page.
The earlier tables embedded their knowledge: "the columns are Name/Age/Occupation," "the rows are these users." That makes them un-reusable — a products table would mean copy-paste-and-edit. A generic table inverts this: the caller describes the columns and supplies the data; the component only knows how to render headers from a config, read row[col.key] for each cell, and apply sort + pagination. Swap the props and it's a different table with zero code changes.
The component owns three pieces of view state — sortKey, asc, page — and derives everything else from props + state in a fixed order: sort the entire data by the active column, then paginate the sorted result. Reverse that order and "page 2 of sorted-by-age" becomes "sort only the rows that happened to land on page 2," which is wrong. Headers and cells are produced by mapping over columns, so the table never names a field directly.
A natural-looking attempt paginates first, then sorts the page:
const pageRows = data.slice(page * size, page * size + size);
const sorted = [...pageRows].sort(byKey); // only sorts THIS page
Each page is sorted in isolation, so "sorted by age" isn't a global order — page 1 has its five sorted, page 2 has a different five sorted, and the oldest person might sit on page 3. Sorting must happen across the whole dataset before you decide which rows fall on which page.
import { useState } from 'react';
import './styles.css';
type Row = { id: number; [key: string]: string | number };
type Column = { key: string; header: string; sortable?: boolean };
function DataTable({
columns,
data,
pageSize,
}: {
columns: Column[];
data: Row[];
pageSize: number;
}) {
const [sortKey, setSortKey] = useState<string | null>(null);
const [asc, setAsc] = useState(true);
const [page, setPage] = useState(0);
const sorted =
sortKey === null
? data
: [...data].sort((a, b) => {
const x = a[sortKey];
const y = b[sortKey];
const cmp =
typeof x === 'number' && typeof y === 'number'
? x - y
: String(x).localeCompare(String(y));
return asc ? cmp : -cmp;
});
const totalPages = Math.ceil(sorted.length / pageSize);
const start = page * pageSize;
const visible = sorted.slice(start, start + pageSize);
function sortBy(key: string) {
if (key === sortKey) {
setAsc((a) => !a);
} else {
setSortKey(key);
setAsc(true);
}
setPage(0); // a new order means a new page 1
}
return (
<>
<table>
<thead>
<tr>
{columns.map((c) => {
const active = c.key === sortKey;
const arrow = active ? (asc ? ' ▲' : ' ▼') : '';
return (
<th
key={c.key}
className={
c.sortable ? (active ? 'sortable active' : 'sortable') : ''
}
onClick={c.sortable ? () => sortBy(c.key) : undefined}
>
{c.header}
{arrow}
</th>
);
})}
</tr>
</thead>
<tbody>
{visible.map((row) => (
<tr key={row.id}>
{columns.map((c) => (
<td key={c.key}>{row[c.key]}</td>
))}
</tr>
))}
</tbody>
</table>
<div className="pager">
<button onClick={() => setPage((p) => Math.max(0, p - 1))} disabled={page === 0}>
Prev
</button>
<span>
Page {page + 1} of {totalPages}
</span>
<button
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
disabled={page >= totalPages - 1}
>
Next
</button>
</div>
</>
);
}
type User = { id: number; name: string; age: number; occupation: string };
const USERS: User[] = [
{ id: 1, name: 'Ada Lovelace', age: 36, occupation: 'Mathematician' },
{ id: 2, name: 'Alan Turing', age: 41, occupation: 'Computer Scientist' },
{ id: 3, name: 'Grace Hopper', age: 85, occupation: 'Rear Admiral' },
{ id: 4, name: 'Katherine Johnson', age: 101, occupation: 'Mathematician' },
{ id: 5, name: 'Linus Torvalds', age: 54, occupation: 'Software Engineer' },
{ id: 6, name: 'Margaret Hamilton', age: 87, occupation: 'Engineer' },
{ id: 7, name: 'Tim Berners-Lee', age: 69, occupation: 'Inventor' },
];
const COLUMNS: Column[] = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'age', header: 'Age', sortable: true },
{ key: 'occupation', header: 'Occupation', sortable: true },
];
export default function App() {
return (
<main className="container">
<h1>Data Table III</h1>
<DataTable columns={COLUMNS} data={USERS} pageSize={5} />
</main>
);
}
DataTable reads nothing about users — columns.map builds the headers (and decides which are clickable via c.sortable), and row[col.key] reads each cell by name from the config. The derivation order is the whole point: sorted first (over all data), then slice(start, start + pageSize). sortBy toggles direction on the same column or switches column, and always setPage(0) so a re-sort doesn't strand you on a now-meaningless page. App is just one line of configuration.
Seven users, pageSize = 5, no sort yet (sortKey = null).
sorted = data (unsorted), totalPages = ceil(7/5) = 2, visible = first 5 in original order. Next is enabled.sortBy('age'): sortKey = 'age', asc = true, page = 0. sorted orders all seven by age ascending (36, 41, 54, 69, 85, 87, 101); visible shows the five youngest.page = 1; start = 5; visible = the two oldest (87, 101) — the globally oldest, because sorting ran over the full set before slicing.asc = false, and setPage(0): now sorted descending from page 1.<DataTable columns={productCols} data={products} pageSize={10} /> would render a products table from the same component.data, then slice.setPage(0) in sortBy.row.name defeats reusability. Fix: row[col.key] from the config.sortable columns shouldn't toggle a sort. Fix: gate onClick/class on c.sortable.disabled={page === totalPages - 1} with 0 rows. With empty data totalPages = 0; use >= so Next stays disabled.render(row) function for badges, links, formatted dates.sortKey/page to props so the parent (or a URL) owns them.import { useState } from 'react';
import './styles.css';
type Row = { id: number; [key: string]: string | number };
type Column = { key: string; header: string; sortable?: boolean };
const USERS: Row[] = [
{ id: 1, name: 'Ada Lovelace', age: 36, occupation: 'Mathematician' },
{ id: 2, name: 'Alan Turing', age: 41, occupation: 'Computer Scientist' },
{ id: 3, name: 'Grace Hopper', age: 85, occupation: 'Rear Admiral' },
{ id: 4, name: 'Katherine Johnson', age: 101, occupation: 'Mathematician' },
{ id: 5, name: 'Linus Torvalds', age: 54, occupation: 'Software Engineer' },
{ id: 6, name: 'Margaret Hamilton', age: 87, occupation: 'Engineer' },
{ id: 7, name: 'Tim Berners-Lee', age: 69, occupation: 'Inventor' },
];
const COLUMNS: Column[] = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'age', header: 'Age', sortable: true },
{ key: 'occupation', header: 'Occupation', sortable: true },
];
function compare(a: Row, b: Row, key: string) {
const x = a[key]; const y = b[key];
return typeof x === 'number' && typeof y === 'number' ? x - y : String(x).localeCompare(String(y));
}
function DataTable({ columns, data, pageSize }: { columns: Column[]; data: Row[]; pageSize: number }) {
const [view, setView] = useState({ sortKey: null as string | null, asc: true, page: 0 });
const ordered = view.sortKey === null ? data : [...data].sort((a, b) => (view.asc ? 1 : -1) * compare(a, b, view.sortKey!));
const totalPages = Math.ceil(ordered.length / pageSize);
const visible = ordered.slice(view.page * pageSize, (view.page + 1) * pageSize);
const sort = (key: string) => setView((current) => ({ sortKey: key, asc: current.sortKey === key ? !current.asc : true, page: 0 }));
return <>
<table><thead><tr>{columns.map((column) => {
const active = column.key === view.sortKey;
return <th key={column.key} className={column.sortable ? active ? 'sortable active' : 'sortable' : ''} onClick={column.sortable ? () => sort(column.key) : undefined}>{column.header}{active ? view.asc ? ' ▲' : ' ▼' : ''}</th>;
})}</tr></thead><tbody>{visible.map((row) => <tr key={row.id}>{columns.map((column) => <td key={column.key}>{row[column.key]}</td>)}</tr>)}</tbody></table>
<div className="pager"><button onClick={() => setView((current) => ({ ...current, page: Math.max(0, current.page - 1) }))} disabled={view.page === 0}>Prev</button><span>Page {view.page + 1} of {totalPages}</span><button onClick={() => setView((current) => ({ ...current, page: Math.min(totalPages - 1, current.page + 1) }))} disabled={view.page >= totalPages - 1}>Next</button></div>
</>;
}
export default function App() {
return <main className="container"><h1>Data Table III</h1><DataTable columns={COLUMNS} data={USERS} pageSize={5} /></main>;
}One view object makes a sort transition reset pagination atomically while every row still comes from the column config.
import { useState } from 'react';
import './styles.css';
type Row = { id: number; [key: string]: string | number };
type Column = { key: string; header: string; sortable?: boolean };
const USERS: Row[] = [
{ id: 1, name: 'Ada Lovelace', age: 36, occupation: 'Mathematician' }, { id: 2, name: 'Alan Turing', age: 41, occupation: 'Computer Scientist' },
{ id: 3, name: 'Grace Hopper', age: 85, occupation: 'Rear Admiral' }, { id: 4, name: 'Katherine Johnson', age: 101, occupation: 'Mathematician' },
{ id: 5, name: 'Linus Torvalds', age: 54, occupation: 'Software Engineer' }, { id: 6, name: 'Margaret Hamilton', age: 87, occupation: 'Engineer' },
{ id: 7, name: 'Tim Berners-Lee', age: 69, occupation: 'Inventor' },
];
const COLUMNS: Column[] = [{ key: 'name', header: 'Name', sortable: true }, { key: 'age', header: 'Age', sortable: true }, { key: 'occupation', header: 'Occupation', sortable: true }];
function useTableModel(data: Row[], pageSize: number) {
const [sortKey, setSortKey] = useState<string | null>(null); const [asc, setAsc] = useState(true); const [page, setPage] = useState(0);
const ordered = sortKey === null ? data : [...data].sort((a, b) => {
const x = a[sortKey], y = b[sortKey]; const value = typeof x === 'number' && typeof y === 'number' ? x - y : String(x).localeCompare(String(y)); return asc ? value : -value;
});
const totalPages = Math.ceil(ordered.length / pageSize);
return { sortKey, asc, page, totalPages, visible: ordered.slice(page * pageSize, (page + 1) * pageSize), sort(key: string) { if (key === sortKey) setAsc((value) => !value); else { setSortKey(key); setAsc(true); } setPage(0); }, prev() { setPage((value) => Math.max(0, value - 1)); }, next() { setPage((value) => Math.min(totalPages - 1, value + 1)); } };
}
function DataTable({ columns, data, pageSize }: { columns: Column[]; data: Row[]; pageSize: number }) {
const model = useTableModel(data, pageSize);
return <><table><thead><tr>{columns.map((column) => { const active = column.key === model.sortKey; return <th key={column.key} className={column.sortable ? active ? 'sortable active' : 'sortable' : ''} onClick={column.sortable ? () => model.sort(column.key) : undefined}>{column.header}{active ? model.asc ? ' ▲' : ' ▼' : ''}</th>; })}</tr></thead><tbody>{model.visible.map((row) => <tr key={row.id}>{columns.map((column) => <td key={column.key}>{row[column.key]}</td>)}</tr>)}</tbody></table><div className="pager"><button onClick={model.prev} disabled={model.page === 0}>Prev</button><span>Page {model.page + 1} of {model.totalPages}</span><button onClick={model.next} disabled={model.page >= model.totalPages - 1}>Next</button></div></>;
}
export default function App() { return <main className="container"><h1>Data Table III</h1><DataTable columns={COLUMNS} data={USERS} pageSize={5} /></main>; }The hook owns the sort then paginate pipeline while the component remains a config driven renderer.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Generalise the table into one reusable component. The previous tables hard-coded the columns and the data; here you pass both in as props — a columns config and a data array — so the same <DataTable> works for users, products, or anything else. It folds together the two earlier features: column-driven sorting (Data Table II) and pagination (Data Table), in that order.
type Column = { key: string; header: string; sortable?: boolean };
type Row = { id: number; [key: string]: string | number };
function DataTable(props: {
columns: Column[];
data: Row[];
pageSize: number;
}): JSX.Element;
<DataTable columns={userCols} data={users} pageSize={5} />
<DataTable columns={productCols} data={products} pageSize={10} />
→ same component, different config, no code changes
sort by Age, then page → page 2 holds the next-oldest five,
NOT a re-sort of page 2's rows in isolation (sort the whole set first)
columns; cells read row[col.key]. No field names baked in.The leap here is from a table that knows about users to a table that knows about nothing — it takes a columns config and a data array and renders whatever you give it. Inside, it runs the two transforms you've already built: sort the whole dataset, then slice out the current page.
The earlier tables embedded their knowledge: "the columns are Name/Age/Occupation," "the rows are these users." That makes them un-reusable — a products table would mean copy-paste-and-edit. A generic table inverts this: the caller describes the columns and supplies the data; the component only knows how to render headers from a config, read row[col.key] for each cell, and apply sort + pagination. Swap the props and it's a different table with zero code changes.
The component owns three pieces of view state — sortKey, asc, page — and derives everything else from props + state in a fixed order: sort the entire data by the active column, then paginate the sorted result. Reverse that order and "page 2 of sorted-by-age" becomes "sort only the rows that happened to land on page 2," which is wrong. Headers and cells are produced by mapping over columns, so the table never names a field directly.
A natural-looking attempt paginates first, then sorts the page:
const pageRows = data.slice(page * size, page * size + size);
const sorted = [...pageRows].sort(byKey); // only sorts THIS page
Each page is sorted in isolation, so "sorted by age" isn't a global order — page 1 has its five sorted, page 2 has a different five sorted, and the oldest person might sit on page 3. Sorting must happen across the whole dataset before you decide which rows fall on which page.
import { useState } from 'react';
import './styles.css';
type Row = { id: number; [key: string]: string | number };
type Column = { key: string; header: string; sortable?: boolean };
function DataTable({
columns,
data,
pageSize,
}: {
columns: Column[];
data: Row[];
pageSize: number;
}) {
const [sortKey, setSortKey] = useState<string | null>(null);
const [asc, setAsc] = useState(true);
const [page, setPage] = useState(0);
const sorted =
sortKey === null
? data
: [...data].sort((a, b) => {
const x = a[sortKey];
const y = b[sortKey];
const cmp =
typeof x === 'number' && typeof y === 'number'
? x - y
: String(x).localeCompare(String(y));
return asc ? cmp : -cmp;
});
const totalPages = Math.ceil(sorted.length / pageSize);
const start = page * pageSize;
const visible = sorted.slice(start, start + pageSize);
function sortBy(key: string) {
if (key === sortKey) {
setAsc((a) => !a);
} else {
setSortKey(key);
setAsc(true);
}
setPage(0); // a new order means a new page 1
}
return (
<>
<table>
<thead>
<tr>
{columns.map((c) => {
const active = c.key === sortKey;
const arrow = active ? (asc ? ' ▲' : ' ▼') : '';
return (
<th
key={c.key}
className={
c.sortable ? (active ? 'sortable active' : 'sortable') : ''
}
onClick={c.sortable ? () => sortBy(c.key) : undefined}
>
{c.header}
{arrow}
</th>
);
})}
</tr>
</thead>
<tbody>
{visible.map((row) => (
<tr key={row.id}>
{columns.map((c) => (
<td key={c.key}>{row[c.key]}</td>
))}
</tr>
))}
</tbody>
</table>
<div className="pager">
<button onClick={() => setPage((p) => Math.max(0, p - 1))} disabled={page === 0}>
Prev
</button>
<span>
Page {page + 1} of {totalPages}
</span>
<button
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
disabled={page >= totalPages - 1}
>
Next
</button>
</div>
</>
);
}
type User = { id: number; name: string; age: number; occupation: string };
const USERS: User[] = [
{ id: 1, name: 'Ada Lovelace', age: 36, occupation: 'Mathematician' },
{ id: 2, name: 'Alan Turing', age: 41, occupation: 'Computer Scientist' },
{ id: 3, name: 'Grace Hopper', age: 85, occupation: 'Rear Admiral' },
{ id: 4, name: 'Katherine Johnson', age: 101, occupation: 'Mathematician' },
{ id: 5, name: 'Linus Torvalds', age: 54, occupation: 'Software Engineer' },
{ id: 6, name: 'Margaret Hamilton', age: 87, occupation: 'Engineer' },
{ id: 7, name: 'Tim Berners-Lee', age: 69, occupation: 'Inventor' },
];
const COLUMNS: Column[] = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'age', header: 'Age', sortable: true },
{ key: 'occupation', header: 'Occupation', sortable: true },
];
export default function App() {
return (
<main className="container">
<h1>Data Table III</h1>
<DataTable columns={COLUMNS} data={USERS} pageSize={5} />
</main>
);
}
DataTable reads nothing about users — columns.map builds the headers (and decides which are clickable via c.sortable), and row[col.key] reads each cell by name from the config. The derivation order is the whole point: sorted first (over all data), then slice(start, start + pageSize). sortBy toggles direction on the same column or switches column, and always setPage(0) so a re-sort doesn't strand you on a now-meaningless page. App is just one line of configuration.
Seven users, pageSize = 5, no sort yet (sortKey = null).
sorted = data (unsorted), totalPages = ceil(7/5) = 2, visible = first 5 in original order. Next is enabled.sortBy('age'): sortKey = 'age', asc = true, page = 0. sorted orders all seven by age ascending (36, 41, 54, 69, 85, 87, 101); visible shows the five youngest.page = 1; start = 5; visible = the two oldest (87, 101) — the globally oldest, because sorting ran over the full set before slicing.asc = false, and setPage(0): now sorted descending from page 1.<DataTable columns={productCols} data={products} pageSize={10} /> would render a products table from the same component.data, then slice.setPage(0) in sortBy.row.name defeats reusability. Fix: row[col.key] from the config.sortable columns shouldn't toggle a sort. Fix: gate onClick/class on c.sortable.disabled={page === totalPages - 1} with 0 rows. With empty data totalPages = 0; use >= so Next stays disabled.render(row) function for badges, links, formatted dates.sortKey/page to props so the parent (or a URL) owns them.import { useState } from 'react';
import './styles.css';
type Row = { id: number; [key: string]: string | number };
type Column = { key: string; header: string; sortable?: boolean };
const USERS: Row[] = [
{ id: 1, name: 'Ada Lovelace', age: 36, occupation: 'Mathematician' },
{ id: 2, name: 'Alan Turing', age: 41, occupation: 'Computer Scientist' },
{ id: 3, name: 'Grace Hopper', age: 85, occupation: 'Rear Admiral' },
{ id: 4, name: 'Katherine Johnson', age: 101, occupation: 'Mathematician' },
{ id: 5, name: 'Linus Torvalds', age: 54, occupation: 'Software Engineer' },
{ id: 6, name: 'Margaret Hamilton', age: 87, occupation: 'Engineer' },
{ id: 7, name: 'Tim Berners-Lee', age: 69, occupation: 'Inventor' },
];
const COLUMNS: Column[] = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'age', header: 'Age', sortable: true },
{ key: 'occupation', header: 'Occupation', sortable: true },
];
function compare(a: Row, b: Row, key: string) {
const x = a[key]; const y = b[key];
return typeof x === 'number' && typeof y === 'number' ? x - y : String(x).localeCompare(String(y));
}
function DataTable({ columns, data, pageSize }: { columns: Column[]; data: Row[]; pageSize: number }) {
const [view, setView] = useState({ sortKey: null as string | null, asc: true, page: 0 });
const ordered = view.sortKey === null ? data : [...data].sort((a, b) => (view.asc ? 1 : -1) * compare(a, b, view.sortKey!));
const totalPages = Math.ceil(ordered.length / pageSize);
const visible = ordered.slice(view.page * pageSize, (view.page + 1) * pageSize);
const sort = (key: string) => setView((current) => ({ sortKey: key, asc: current.sortKey === key ? !current.asc : true, page: 0 }));
return <>
<table><thead><tr>{columns.map((column) => {
const active = column.key === view.sortKey;
return <th key={column.key} className={column.sortable ? active ? 'sortable active' : 'sortable' : ''} onClick={column.sortable ? () => sort(column.key) : undefined}>{column.header}{active ? view.asc ? ' ▲' : ' ▼' : ''}</th>;
})}</tr></thead><tbody>{visible.map((row) => <tr key={row.id}>{columns.map((column) => <td key={column.key}>{row[column.key]}</td>)}</tr>)}</tbody></table>
<div className="pager"><button onClick={() => setView((current) => ({ ...current, page: Math.max(0, current.page - 1) }))} disabled={view.page === 0}>Prev</button><span>Page {view.page + 1} of {totalPages}</span><button onClick={() => setView((current) => ({ ...current, page: Math.min(totalPages - 1, current.page + 1) }))} disabled={view.page >= totalPages - 1}>Next</button></div>
</>;
}
export default function App() {
return <main className="container"><h1>Data Table III</h1><DataTable columns={COLUMNS} data={USERS} pageSize={5} /></main>;
}One view object makes a sort transition reset pagination atomically while every row still comes from the column config.
import { useState } from 'react';
import './styles.css';
type Row = { id: number; [key: string]: string | number };
type Column = { key: string; header: string; sortable?: boolean };
const USERS: Row[] = [
{ id: 1, name: 'Ada Lovelace', age: 36, occupation: 'Mathematician' }, { id: 2, name: 'Alan Turing', age: 41, occupation: 'Computer Scientist' },
{ id: 3, name: 'Grace Hopper', age: 85, occupation: 'Rear Admiral' }, { id: 4, name: 'Katherine Johnson', age: 101, occupation: 'Mathematician' },
{ id: 5, name: 'Linus Torvalds', age: 54, occupation: 'Software Engineer' }, { id: 6, name: 'Margaret Hamilton', age: 87, occupation: 'Engineer' },
{ id: 7, name: 'Tim Berners-Lee', age: 69, occupation: 'Inventor' },
];
const COLUMNS: Column[] = [{ key: 'name', header: 'Name', sortable: true }, { key: 'age', header: 'Age', sortable: true }, { key: 'occupation', header: 'Occupation', sortable: true }];
function useTableModel(data: Row[], pageSize: number) {
const [sortKey, setSortKey] = useState<string | null>(null); const [asc, setAsc] = useState(true); const [page, setPage] = useState(0);
const ordered = sortKey === null ? data : [...data].sort((a, b) => {
const x = a[sortKey], y = b[sortKey]; const value = typeof x === 'number' && typeof y === 'number' ? x - y : String(x).localeCompare(String(y)); return asc ? value : -value;
});
const totalPages = Math.ceil(ordered.length / pageSize);
return { sortKey, asc, page, totalPages, visible: ordered.slice(page * pageSize, (page + 1) * pageSize), sort(key: string) { if (key === sortKey) setAsc((value) => !value); else { setSortKey(key); setAsc(true); } setPage(0); }, prev() { setPage((value) => Math.max(0, value - 1)); }, next() { setPage((value) => Math.min(totalPages - 1, value + 1)); } };
}
function DataTable({ columns, data, pageSize }: { columns: Column[]; data: Row[]; pageSize: number }) {
const model = useTableModel(data, pageSize);
return <><table><thead><tr>{columns.map((column) => { const active = column.key === model.sortKey; return <th key={column.key} className={column.sortable ? active ? 'sortable active' : 'sortable' : ''} onClick={column.sortable ? () => model.sort(column.key) : undefined}>{column.header}{active ? model.asc ? ' ▲' : ' ▼' : ''}</th>; })}</tr></thead><tbody>{model.visible.map((row) => <tr key={row.id}>{columns.map((column) => <td key={column.key}>{row[column.key]}</td>)}</tr>)}</tbody></table><div className="pager"><button onClick={model.prev} disabled={model.page === 0}>Prev</button><span>Page {model.page + 1} of {model.totalPages}</span><button onClick={model.next} disabled={model.page >= model.totalPages - 1}>Next</button></div></>;
}
export default function App() { return <main className="container"><h1>Data Table III</h1><DataTable columns={COLUMNS} data={USERS} pageSize={5} /></main>; }The hook owns the sort then paginate pipeline while the component remains a config driven renderer.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.