Build a table generator: two number inputs — rows and columns — and a table that fills with sequential numbers to match. Change either input and the grid redraws. The whole trick is a single mapping: the cell at row r, column c holds r * cols + c + 1, which turns a 2-D position into the running count 1, 2, 3, ….
// A self-contained component. No props.
function App(): JSX.Element;
Two controlled number inputs (rows, cols) and a <table> derived from them.
rows = 3, cols = 4 →
1 2 3 4
5 6 7 8
9 10 11 12
rows = 2, cols = 2 →
1 2
3 4
r * cols + c + 1.rows and cols and compute the cells on render.The table isn't data you store — it's a view of two numbers. Keep rows and cols in state, and compute every cell on render from its position. One formula turns a (row, column) coordinate into the sequential number that belongs there.
You have two inputs and a grid that must always agree with them. If you stored the grid as a 2-D array, every input change would mean rebuilding that array and keeping it in sync — a second source of truth that can drift. Instead, store only the counts. The grid is then a pure function of rows and cols, so it can never disagree with the inputs: there's nothing to keep in sync because there's only one copy of the truth.
Two pieces of state: rows and cols. To draw the table, make a range of row indices 0 … rows-1 and a range of column indices 0 … cols-1, then nest them: for each row index r, map the column indices to cells. The number in each cell is r * cols + c + 1 — r * cols skips all the cells in the rows above, + c walks across the current row, and + 1 shifts from 0-based math to 1-based display.
A common first move is to keep the grid itself in state and rebuild it whenever an input changes:
const [grid, setGrid] = useState([[1, 2], [3, 4]]);
function onRowsChange(n) {
// now rebuild the entire 2-D array by hand…
// and remember to do it again when cols changes…
}
Now there are three things in state — rows, cols, and grid — and two of them must be kept consistent with the third by hand. Forget one path and the grid shows stale dimensions. Storing only the counts deletes the whole class of bug.
import { useState } from 'react';
import './styles.css';
export default function App() {
const [rows, setRows] = useState(3);
const [cols, setCols] = useState(4);
const rowList = Array.from({ length: rows }, (_, r) => r);
const colList = Array.from({ length: cols }, (_, c) => c);
return (
<main className="container">
<h1>Generate Table</h1>
<div className="controls">
<div className="field">
<label htmlFor="rows">Rows</label>
<input
id="rows"
type="number"
min={1}
value={rows}
onChange={(e) => setRows(Math.max(1, Number(e.target.value)))}
/>
</div>
<div className="field">
<label htmlFor="cols">Columns</label>
<input
id="cols"
type="number"
min={1}
value={cols}
onChange={(e) => setCols(Math.max(1, Number(e.target.value)))}
/>
</div>
</div>
<table>
<tbody>
{rowList.map((r) => (
<tr key={r}>
{colList.map((c) => (
<td key={c}>{r * cols + c + 1}</td>
))}
</tr>
))}
</tbody>
</table>
</main>
);
}
Array.from({ length: rows }, (_, r) => r) makes [0, 1, 2, …] — a range to map over, since you can't .map a bare number. The inputs are controlled: value={rows} ties the field to state, and onChange reads e.target.value (always a string from the DOM), coerces it with Number(...), and clamps with Math.max(1, …) so the table can't collapse. The nested map renders rows then columns, and each cell computes its own number from r and c.
Say rows = 3, cols = 4.
rowList = [0, 1, 2], colList = [0, 1, 2, 3].r = 0, emit a <tr> and map the columns: cells are 0*4 + c + 1 → 1, 2, 3, 4.r = 1: 1*4 + c + 1 → 5, 6, 7, 8. The r * cols term (4) is exactly the count of cells already placed.r = 2: 9, 10, 11, 12.onChange sets rows = 5; re-render rebuilds rowList as [0…4] and two more rows appear, numbered 13 … 20. Nothing else had to change.Because the cells are computed from rows and cols every render, the grid is always exactly right for the current inputs.
rows/cols is a second source of truth that drifts. Fix: store only the counts; derive the grid.rows.map(...) throws — numbers aren't iterable. Fix: Array.from({ length: rows }, (_, i) => i) to get a range.r * cols + c starts at 0; the + 1 makes it human-readable. Drop it and the table reads 0 … 11.value={rows}, the field and state diverge. Fix: controlled input with value + onChange.NaN/0 rows and a blank table. Fix: Math.max(1, Number(...)).c * rows + r + 1 to number down each column first.<thead> with column labels (A, B, C …) derived the same way.This version groups rows and columns in a reducer. The table is still derived during render and produces identical markup.
import { useReducer } from 'react';
import './styles.css';
type State = { rows: number; cols: number };
type Action = { key: keyof State; value: number };
export default function App() {
const [state, dispatch] = useReducer((current: State, action: Action) => ({ ...current, [action.key]: Math.max(1, action.value) }), { rows: 3, cols: 4 });
const rowList = Array.from({ length: state.rows }, (_, r) => r);
const colList = Array.from({ length: state.cols }, (_, c) => c);
const set = (key: keyof State) => (e: React.ChangeEvent<HTMLInputElement>) => dispatch({ key, value: Number(e.target.value) });
return (
<main className="container">
<h1>Generate Table</h1>
<div className="controls">
<div className="field"><label htmlFor="rows">Rows</label><input id="rows" type="number" min={1} value={state.rows} onChange={set('rows')} /></div>
<div className="field"><label htmlFor="cols">Columns</label><input id="cols" type="number" min={1} value={state.cols} onChange={set('cols')} /></div>
</div>
<table><tbody>{rowList.map((r) => <tr key={r}>{colList.map((c) => <td key={c}>{r * state.cols + c + 1}</td>)}</tr>)}</tbody></table>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Build a table generator: two number inputs — rows and columns — and a table that fills with sequential numbers to match. Change either input and the grid redraws. The whole trick is a single mapping: the cell at row r, column c holds r * cols + c + 1, which turns a 2-D position into the running count 1, 2, 3, ….
// A self-contained component. No props.
function App(): JSX.Element;
Two controlled number inputs (rows, cols) and a <table> derived from them.
rows = 3, cols = 4 →
1 2 3 4
5 6 7 8
9 10 11 12
rows = 2, cols = 2 →
1 2
3 4
r * cols + c + 1.rows and cols and compute the cells on render.The table isn't data you store — it's a view of two numbers. Keep rows and cols in state, and compute every cell on render from its position. One formula turns a (row, column) coordinate into the sequential number that belongs there.
You have two inputs and a grid that must always agree with them. If you stored the grid as a 2-D array, every input change would mean rebuilding that array and keeping it in sync — a second source of truth that can drift. Instead, store only the counts. The grid is then a pure function of rows and cols, so it can never disagree with the inputs: there's nothing to keep in sync because there's only one copy of the truth.
Two pieces of state: rows and cols. To draw the table, make a range of row indices 0 … rows-1 and a range of column indices 0 … cols-1, then nest them: for each row index r, map the column indices to cells. The number in each cell is r * cols + c + 1 — r * cols skips all the cells in the rows above, + c walks across the current row, and + 1 shifts from 0-based math to 1-based display.
A common first move is to keep the grid itself in state and rebuild it whenever an input changes:
const [grid, setGrid] = useState([[1, 2], [3, 4]]);
function onRowsChange(n) {
// now rebuild the entire 2-D array by hand…
// and remember to do it again when cols changes…
}
Now there are three things in state — rows, cols, and grid — and two of them must be kept consistent with the third by hand. Forget one path and the grid shows stale dimensions. Storing only the counts deletes the whole class of bug.
import { useState } from 'react';
import './styles.css';
export default function App() {
const [rows, setRows] = useState(3);
const [cols, setCols] = useState(4);
const rowList = Array.from({ length: rows }, (_, r) => r);
const colList = Array.from({ length: cols }, (_, c) => c);
return (
<main className="container">
<h1>Generate Table</h1>
<div className="controls">
<div className="field">
<label htmlFor="rows">Rows</label>
<input
id="rows"
type="number"
min={1}
value={rows}
onChange={(e) => setRows(Math.max(1, Number(e.target.value)))}
/>
</div>
<div className="field">
<label htmlFor="cols">Columns</label>
<input
id="cols"
type="number"
min={1}
value={cols}
onChange={(e) => setCols(Math.max(1, Number(e.target.value)))}
/>
</div>
</div>
<table>
<tbody>
{rowList.map((r) => (
<tr key={r}>
{colList.map((c) => (
<td key={c}>{r * cols + c + 1}</td>
))}
</tr>
))}
</tbody>
</table>
</main>
);
}
Array.from({ length: rows }, (_, r) => r) makes [0, 1, 2, …] — a range to map over, since you can't .map a bare number. The inputs are controlled: value={rows} ties the field to state, and onChange reads e.target.value (always a string from the DOM), coerces it with Number(...), and clamps with Math.max(1, …) so the table can't collapse. The nested map renders rows then columns, and each cell computes its own number from r and c.
Say rows = 3, cols = 4.
rowList = [0, 1, 2], colList = [0, 1, 2, 3].r = 0, emit a <tr> and map the columns: cells are 0*4 + c + 1 → 1, 2, 3, 4.r = 1: 1*4 + c + 1 → 5, 6, 7, 8. The r * cols term (4) is exactly the count of cells already placed.r = 2: 9, 10, 11, 12.onChange sets rows = 5; re-render rebuilds rowList as [0…4] and two more rows appear, numbered 13 … 20. Nothing else had to change.Because the cells are computed from rows and cols every render, the grid is always exactly right for the current inputs.
rows/cols is a second source of truth that drifts. Fix: store only the counts; derive the grid.rows.map(...) throws — numbers aren't iterable. Fix: Array.from({ length: rows }, (_, i) => i) to get a range.r * cols + c starts at 0; the + 1 makes it human-readable. Drop it and the table reads 0 … 11.value={rows}, the field and state diverge. Fix: controlled input with value + onChange.NaN/0 rows and a blank table. Fix: Math.max(1, Number(...)).c * rows + r + 1 to number down each column first.<thead> with column labels (A, B, C …) derived the same way.This version groups rows and columns in a reducer. The table is still derived during render and produces identical markup.
import { useReducer } from 'react';
import './styles.css';
type State = { rows: number; cols: number };
type Action = { key: keyof State; value: number };
export default function App() {
const [state, dispatch] = useReducer((current: State, action: Action) => ({ ...current, [action.key]: Math.max(1, action.value) }), { rows: 3, cols: 4 });
const rowList = Array.from({ length: state.rows }, (_, r) => r);
const colList = Array.from({ length: state.cols }, (_, c) => c);
const set = (key: keyof State) => (e: React.ChangeEvent<HTMLInputElement>) => dispatch({ key, value: Number(e.target.value) });
return (
<main className="container">
<h1>Generate Table</h1>
<div className="controls">
<div className="field"><label htmlFor="rows">Rows</label><input id="rows" type="number" min={1} value={state.rows} onChange={set('rows')} /></div>
<div className="field"><label htmlFor="cols">Columns</label><input id="cols" type="number" min={1} value={state.cols} onChange={set('cols')} /></div>
</div>
<table><tbody>{rowList.map((r) => <tr key={r}>{colList.map((c) => <td key={c}>{r * state.cols + c + 1}</td>)}</tr>)}</tbody></table>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.