Build a date range picker
mediumState & DataHigh freq+200 XP
Build a date range picker — a start date input and an end date input.
Both are controlled text inputs that accept an ISO-shaped date (YYYY-MM-DD).
The end date must not be before the start date; when the user tries, surface a visible error and don't update the committed range.
Examples
// Both inputs empty → summary = "" , no error.
// start = "2026-01-10", end = "2026-01-20"
// → summary reads "2026-01-10 to 2026-01-20"
// Change end to "2026-01-05"
// → summary clears, error appears:
// "End date cannot be before start date"
// Fix end to "2026-02-01"
// → error clears, summary updates.The committed range and the error are mutually exclusive — never both at once.
const hasBoth = start !== "" && end !== "";
const isInvalid = hasBoth && end < start;
const summary = hasBoth && !isInvalid ? `${start} to ${end}` : "";
const error = isInvalid ? "End date cannot be before start date" : "";ISO YYYY-MM-DD strings compare correctly as plain strings — no `Date` parsing needed.
What it has to do
- The start input is `<input data-testid="start-date" />`.
- The end input is `<input data-testid="end-date" />`.
- An element `<p data-testid="range-summary">` displays the committed range as `"<start> to <end>"` when both are set, or an empty string when either is missing.
- If the user types an end date earlier than the start, show `<p data-testid="range-error">End date cannot be before start date</p>` and leave the committed range unchanged.
- Clearing one or both inputs clears the error and the summary accordingly — never show both an error and a committed range at the same time.
Notes
- Derive both `summary` and `error` from the inputs; don't track them as separate state. Separate state means two places can disagree.
- Validate on each change rather than on blur — users expect the error to appear and clear as they type, not surprise them later.
- Timezone-safe comparison matters once you move past string compare: compare day boundaries, not full `Date` instants, or a user west of UTC can accidentally cross midnight on "end === start".
- An accessible error should sit in a region announced to assistive tech (`aria-live="polite"` on a wrapper), and the error text should be tied to the end input with `aria-describedby`.
Stuck? The react course covers the ideas this problem is built on.
import { useState } from "react"; export default function App() { const [start, setStart] = useState(""); const [end, setEnd] = useState(""); // TODO: compute isValid, summary, error from start/end. // Remember: never show both a committed range and an error at the same time. const summary = ""; const error = ""; return ( <div style={{ fontFamily: "sans-serif", padding: 16, display: "grid", gap: 8, maxWidth: 320 }}> <label> Start:{" "} <input type="date" data-testid="start-date" value={start} onChange={(e) => setStart(e.target.value)} /> </label> <label> End:{" "} <input type="date" data-testid="end-date" value={end} onChange={(e) => setEnd(e.target.value)} /> </label> <p data-testid="range-summary">{summary}</p> {error ? <p data-testid="range-error" style={{ color: "crimson" }}>{error}</p> : null} </div> ); }
- 1.The start input is `<input data-testid="start-date" />`.
- 2.The end input is `<input data-testid="end-date" />`.
- 3.An element `<p data-testid="range-summary">` displays the committed range as `"<start> to <end>"` when both are set, or an empty string when either is missing.
- 4.If the user types an end date earlier than the start, show `<p data-testid="range-error">End date cannot be before start date</p>` and leave the committed range unchanged.
- 5.Clearing one or both inputs clears the error and the summary accordingly — never show both an error and a committed range at the same time.
5 automated assertions run against your rendered component. The exact assertion names are hidden so you build to the behaviour, not to the test.
5 tests · +200 XP
Discussion
Sign in to postSign in to join the conversation.Sign in
No comments yet. Be the first to say something.