Build a dice roller: pick how many six-sided dice to roll, hit Roll, and see the faces (and total). The state is just the chosen count and the array of rolled values; rolling fills that array with count random numbers from 1–6, and each value renders as a die face.
// A self-contained component. No props.
function App(): JSX.Element;
A count selector, a Roll button, the dice faces, and a total.
count 3, Roll -> e.g. [4, 1, 6] -> faces, total 11
change count to 5, Roll -> an array of 5 fresh values
each value v -> Math.floor(Math.random() * 6) + 1 (1..6 inclusive)
face for v -> FACES[v - 1]
count from the select; dice is an array of length count after a roll.count randoms each Roll; don't mutate the old one.1..6 inclusive. Math.floor(Math.random() * 6) + 1 — the + 1 shifts 0–5 to 1–6.total = dice.reduce((a, b) => a + b, 0).Two pieces of state: how many dice (count) and the last rolled values (dice, an array). Rolling builds a fresh array of count random numbers in 1–6; rendering maps each number to a die face and sums them for the total.
A dice roll is "make N random numbers, each 1 to 6." So count (from the selector) decides the array length, and Roll regenerates the array. Everything shown is derived: each value becomes a face glyph, and the total is the sum. The only easy mistakes are the random range (off-by-one if you forget the + 1) and mutating the old array instead of producing a new one.
State: count (a number from the <select>) and dice (number[], the rolled values). roll() does Array.from({ length: count }, () => Math.floor(Math.random() * 6) + 1) and stores it. Render: dice.map(v => FACES[v - 1]) for the glyphs, and total = dice.reduce((a, b) => a + b, 0). Changing the count just updates count; the next Roll uses it.
A first attempt rolls one die into a single value, or mutates in place:
const [value, setValue] = useState(1);
const roll = () => setValue(Math.ceil(Math.random() * 6)); // only one die
A single value can't represent "5 dice," and pushing into an existing array (dice.push(...)) both mutates state (no re-render) and grows without bound across rolls. Storing an array sized by count, rebuilt each Roll, models any number of dice and keeps each roll independent.
import { useState } from 'react';
import './styles.css';
const COUNTS = [1, 2, 3, 4, 5];
const FACES = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
export default function App() {
const [count, setCount] = useState(2);
const [dice, setDice] = useState<number[]>([]);
function roll() {
setDice(Array.from({ length: count }, () => Math.floor(Math.random() * 6) + 1));
}
const total = dice.reduce((sum, v) => sum + v, 0);
return (
<main className="container">
<h1>Dice Roller</h1>
<div className="controls">
<label htmlFor="count">Dice</label>
<select
id="count"
value={count}
onChange={(e) => setCount(Number(e.target.value))}
>
{COUNTS.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
<button onClick={roll}>Roll</button>
</div>
<div className="dice">
{dice.map((v, i) => (
<span className="die" key={i}>
{FACES[v - 1]}
</span>
))}
</div>
<p className="total">
Total: <span>{total}</span>
</p>
</main>
);
}
roll rebuilds dice from scratch with Array.from, so each click is an independent roll of count dice; Math.floor(Math.random() * 6) + 1 yields 1–6 inclusive. The <select> is controlled (value={count} + onChange coercing with Number). Rendering maps each value to FACES[v - 1] (value 1 → index 0), and total is a reduce sum — both derived, so they always match dice.
count = 2, dice = [].
roll() builds Array.from({ length: 2 }, …) → e.g. [4, 1]. Faces ⚃ ⚀ render; total 4 + 1 = 5.onChange → count = 5 (the array is still the old [4, 1] until the next roll).[6, 3, 3, 2, 5] → five faces, total 19.number[] sized by count.Math.floor(random()*6) gives 0–5; forgetting + 1 shows blank/undefined faces. Fix: + 1.dice. No re-render and unbounded growth. Fix: build a new array each Roll.[value] on an Angular <select>. Won't reflect the choice; bind [selected] per option (see the Angular variant).total from dice each render, don't keep it in separate state.One model holds the chosen count and the latest immutable roll. Count changes preserve the current display until Roll replaces it, matching the original interaction exactly.
import { useState } from 'react';
import './styles.css';
const COUNTS = [1, 2, 3, 4, 5];
const FACES = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
type Roll = { count: number; dice: number[] };
export default function App() {
const [model, setModel] = useState<Roll>({ count: 2, dice: [] });
const chooseCount = (count: number) => setModel((current) => ({ ...current, count }));
const roll = () => {
const dice = Array.from({ length: model.count }, () => Math.floor(Math.random() * 6) + 1);
setModel((current) => ({ ...current, dice }));
};
return (
<main className="container">
<h1>Dice Roller</h1>
<div className="controls">
<label htmlFor="count">Dice</label>
<select id="count" value={model.count} onChange={(event) => chooseCount(Number(event.target.value))}>
{COUNTS.map((count) => <option key={count} value={count}>{count}</option>)}
</select>
<button onClick={roll}>Roll</button>
</div>
<div className="dice">{model.dice.map((value, index) => <span className="die" key={index}>{FACES[value - 1]}</span>)}</div>
<p className="total">Total: <span>{model.dice.reduce((sum, value) => sum + value, 0)}</span></p>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Build a dice roller: pick how many six-sided dice to roll, hit Roll, and see the faces (and total). The state is just the chosen count and the array of rolled values; rolling fills that array with count random numbers from 1–6, and each value renders as a die face.
// A self-contained component. No props.
function App(): JSX.Element;
A count selector, a Roll button, the dice faces, and a total.
count 3, Roll -> e.g. [4, 1, 6] -> faces, total 11
change count to 5, Roll -> an array of 5 fresh values
each value v -> Math.floor(Math.random() * 6) + 1 (1..6 inclusive)
face for v -> FACES[v - 1]
count from the select; dice is an array of length count after a roll.count randoms each Roll; don't mutate the old one.1..6 inclusive. Math.floor(Math.random() * 6) + 1 — the + 1 shifts 0–5 to 1–6.total = dice.reduce((a, b) => a + b, 0).Two pieces of state: how many dice (count) and the last rolled values (dice, an array). Rolling builds a fresh array of count random numbers in 1–6; rendering maps each number to a die face and sums them for the total.
A dice roll is "make N random numbers, each 1 to 6." So count (from the selector) decides the array length, and Roll regenerates the array. Everything shown is derived: each value becomes a face glyph, and the total is the sum. The only easy mistakes are the random range (off-by-one if you forget the + 1) and mutating the old array instead of producing a new one.
State: count (a number from the <select>) and dice (number[], the rolled values). roll() does Array.from({ length: count }, () => Math.floor(Math.random() * 6) + 1) and stores it. Render: dice.map(v => FACES[v - 1]) for the glyphs, and total = dice.reduce((a, b) => a + b, 0). Changing the count just updates count; the next Roll uses it.
A first attempt rolls one die into a single value, or mutates in place:
const [value, setValue] = useState(1);
const roll = () => setValue(Math.ceil(Math.random() * 6)); // only one die
A single value can't represent "5 dice," and pushing into an existing array (dice.push(...)) both mutates state (no re-render) and grows without bound across rolls. Storing an array sized by count, rebuilt each Roll, models any number of dice and keeps each roll independent.
import { useState } from 'react';
import './styles.css';
const COUNTS = [1, 2, 3, 4, 5];
const FACES = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
export default function App() {
const [count, setCount] = useState(2);
const [dice, setDice] = useState<number[]>([]);
function roll() {
setDice(Array.from({ length: count }, () => Math.floor(Math.random() * 6) + 1));
}
const total = dice.reduce((sum, v) => sum + v, 0);
return (
<main className="container">
<h1>Dice Roller</h1>
<div className="controls">
<label htmlFor="count">Dice</label>
<select
id="count"
value={count}
onChange={(e) => setCount(Number(e.target.value))}
>
{COUNTS.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
<button onClick={roll}>Roll</button>
</div>
<div className="dice">
{dice.map((v, i) => (
<span className="die" key={i}>
{FACES[v - 1]}
</span>
))}
</div>
<p className="total">
Total: <span>{total}</span>
</p>
</main>
);
}
roll rebuilds dice from scratch with Array.from, so each click is an independent roll of count dice; Math.floor(Math.random() * 6) + 1 yields 1–6 inclusive. The <select> is controlled (value={count} + onChange coercing with Number). Rendering maps each value to FACES[v - 1] (value 1 → index 0), and total is a reduce sum — both derived, so they always match dice.
count = 2, dice = [].
roll() builds Array.from({ length: 2 }, …) → e.g. [4, 1]. Faces ⚃ ⚀ render; total 4 + 1 = 5.onChange → count = 5 (the array is still the old [4, 1] until the next roll).[6, 3, 3, 2, 5] → five faces, total 19.number[] sized by count.Math.floor(random()*6) gives 0–5; forgetting + 1 shows blank/undefined faces. Fix: + 1.dice. No re-render and unbounded growth. Fix: build a new array each Roll.[value] on an Angular <select>. Won't reflect the choice; bind [selected] per option (see the Angular variant).total from dice each render, don't keep it in separate state.One model holds the chosen count and the latest immutable roll. Count changes preserve the current display until Roll replaces it, matching the original interaction exactly.
import { useState } from 'react';
import './styles.css';
const COUNTS = [1, 2, 3, 4, 5];
const FACES = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
type Roll = { count: number; dice: number[] };
export default function App() {
const [model, setModel] = useState<Roll>({ count: 2, dice: [] });
const chooseCount = (count: number) => setModel((current) => ({ ...current, count }));
const roll = () => {
const dice = Array.from({ length: model.count }, () => Math.floor(Math.random() * 6) + 1);
setModel((current) => ({ ...current, dice }));
};
return (
<main className="container">
<h1>Dice Roller</h1>
<div className="controls">
<label htmlFor="count">Dice</label>
<select id="count" value={model.count} onChange={(event) => chooseCount(Number(event.target.value))}>
{COUNTS.map((count) => <option key={count} value={count}>{count}</option>)}
</select>
<button onClick={roll}>Roll</button>
</div>
<div className="dice">{model.dice.map((value, index) => <span className="die" key={index}>{FACES[value - 1]}</span>)}</div>
<p className="total">Total: <span>{model.dice.reduce((sum, value) => sum + value, 0)}</span></p>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.