Build a small flight booker — the classic 7GUIs exercise. A dropdown chooses a one-way or return trip, and there are two date fields: departure and return. The two fields are linked: the return date is disabled for a one-way trip, and for a return trip you can't book a flight that comes back before it leaves. It's a compact lesson in dependent fields and cross-field validation — where one control's state changes what another can do.
// A self-contained component. No props.
function App(): JSX.Element;
A trip-type select, two date inputs, and a Book button that's enabled only when the selection is valid.
type = one-way → return date disabled; Book enabled
type = return, dep ≤ ret → both dates active; Book enabled
type = return, dep > ret → return field flagged; Book DISABLED
click Book → "Booked a return flight departing … returning …"
yyyy-mm-dd strings compare correctly with <, so:
'2026-07-01' < '2026-07-08' → valid return trip
yyyy-mm-dd) sort lexically, a plain ret < depart comparison works — no Date parsing needed.You'll build a form where one control reshapes another — the return field switches on and off with the trip type, and the Book button reflects a rule that spans two fields.
There are three controls: a trip-type dropdown and two dates. They're not independent. Picking "one-way" makes the return date meaningless, so it should be disabled. Picking "return" turns it on, but now a new rule applies: you can't return before you depart, so the Book button has to refuse an invalid pair. The skill is letting these relationships fall out of a little derived state rather than scattering enable/disable flags around.
Keep only the raw inputs in state: type, depart, and ret. Everything else — whether the return field is enabled, whether the dates are valid, whether Book is allowed — is derived from those three on each render. ISO date strings compare correctly with <, so the validation is a one-liner. State holds the facts; the UI's enabled/disabled/invalid states are computed views of them.
A first pass often leaves both dates always editable and skips the cross-field rule:
export default function App() {
const [type, setType] = useState('one-way');
const [depart, setDepart] = useState('');
const [ret, setRet] = useState('');
return (
<>
<select value={type} onChange={(e) => setType(e.target.value)}>…</select>
<input type="date" value={depart} onChange={(e) => setDepart(e.target.value)} />
<input type="date" value={ret} onChange={(e) => setRet(e.target.value)} />
<button onClick={book}>Book</button> {/* always enabled */}
</>
);
}
This "works" but lets the user book nonsense: a return date is editable even for a one-way trip, and you can book a return flight that comes home before it leaves. The fields look independent, but the spec says they aren't — the missing piece is derived enable/validate logic tying them together.
import { useState } from 'react';
import './styles.css';
export default function App() {
const [type, setType] = useState('one-way');
const [depart, setDepart] = useState('2026-07-01');
const [ret, setRet] = useState('2026-07-08');
const [booked, setBooked] = useState(null);
// Derived from the inputs — nothing stored.
const isReturn = type === 'return';
const datesInvalid = isReturn && ret < depart; // ISO strings compare correctly
const canBook = depart !== '' && (!isReturn || (ret !== '' && !datesInvalid));
function book() {
setBooked(
isReturn
? `Booked a return flight departing ${depart}, returning ${ret}.`
: `Booked a one-way flight on ${depart}.`,
);
}
if (booked) {
return (
<main className="container">
<h1>Flight Booker</h1>
<div className="booked">
<h2>✈ Confirmed</h2>
<p>{booked}</p>
<button onClick={() => setBooked(null)}>Book another</button>
</div>
</main>
);
}
return (
<main className="container">
<h1>Flight Booker</h1>
<div className="flight-booker">
<select value={type} onChange={(e) => setType(e.target.value)}>
<option value="one-way">One-way flight</option>
<option value="return">Return flight</option>
</select>
<input type="date" value={depart} onChange={(e) => setDepart(e.target.value)} />
<input
type="date"
value={ret}
disabled={!isReturn}
className={datesInvalid ? 'invalid' : ''}
onChange={(e) => setRet(e.target.value)}
/>
<button className="book" type="button" disabled={!canBook} onClick={book}>
Book
</button>
</div>
</main>
);
}
The shifts: isReturn, datesInvalid, and canBook are computed each render from the three inputs, so the return field's disabled, its invalid class, and the Book button's disabled all stay correct automatically. The cross-field rule is the single expression ret < depart, made trivial by ISO date strings sorting lexically.
Start at type = 'one-way', depart = '2026-07-01', ret = '2026-07-08'.
isReturn is false, so the return input is disabled; datesInvalid is false; canBook is depart !== '' → true. Book is enabled.setType('return') re-renders. Now isReturn is true, the return input enables, and canBook checks the dates: '2026-07-08' < '2026-07-01' is false, so still valid — Book stays enabled.2026-06-20. datesInvalid becomes '2026-06-20' < '2026-07-01' → true. The return field gets the invalid class (red border) and canBook is false — Book disables.2026-07-10. datesInvalid flips back to false; Book re-enables. Clicking it sets booked to the return-trip message and the confirmation view renders.Every enable/disable/invalid state recomputed itself from type/depart/ret — no manual flag juggling.
disabled={!isReturn}.datesInvalid and gate the Book button on it.new Date(a) > new Date(b) works but is overkill. Fix: ISO yyyy-mm-dd strings compare directly with <.canBook/isValid in state. It drifts from the inputs. Fix: derive it during render.min to today so past dates can't be chosen, and the return's min to the departure date.This version groups the raw form values and confirmation into one state record. All enabled, invalid, and bookable values remain derived, and the rendered UI is unchanged.
import { useState } from 'react';
import './styles.css';
type BookingState = { type: string; depart: string; ret: string; booked: string | null };
export default function App() {
const [state, setState] = useState<BookingState>({ type: 'one-way', depart: '2026-07-01', ret: '2026-07-08', booked: null });
const update = (patch: Partial<BookingState>) => setState((current) => ({ ...current, ...patch }));
const isReturn = state.type === 'return';
const datesInvalid = isReturn && state.ret < state.depart;
const canBook = state.depart !== '' && (!isReturn || (state.ret !== '' && !datesInvalid));
function book() {
update({ booked: isReturn
? `Booked a return flight departing ${state.depart}, returning ${state.ret}.`
: `Booked a one-way flight on ${state.depart}.` });
}
return (
<main className="container">
<h1>Flight Booker</h1>
{state.booked ? (
<div className="booked"><h2>✈ Confirmed</h2><p>{state.booked}</p><button onClick={() => update({ booked: null })}>Book another</button></div>
) : (
<div className="flight-booker">
<select value={state.type} onChange={(e) => update({ type: e.target.value })}>
<option value="one-way">One-way flight</option><option value="return">Return flight</option>
</select>
<input type="date" value={state.depart} onChange={(e) => update({ depart: e.target.value })} />
<input type="date" value={state.ret} disabled={!isReturn} className={datesInvalid ? 'invalid' : ''} onChange={(e) => update({ ret: e.target.value })} />
<button className="book" type="button" disabled={!canBook} onClick={book}>Book</button>
</div>
)}
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Build a small flight booker — the classic 7GUIs exercise. A dropdown chooses a one-way or return trip, and there are two date fields: departure and return. The two fields are linked: the return date is disabled for a one-way trip, and for a return trip you can't book a flight that comes back before it leaves. It's a compact lesson in dependent fields and cross-field validation — where one control's state changes what another can do.
// A self-contained component. No props.
function App(): JSX.Element;
A trip-type select, two date inputs, and a Book button that's enabled only when the selection is valid.
type = one-way → return date disabled; Book enabled
type = return, dep ≤ ret → both dates active; Book enabled
type = return, dep > ret → return field flagged; Book DISABLED
click Book → "Booked a return flight departing … returning …"
yyyy-mm-dd strings compare correctly with <, so:
'2026-07-01' < '2026-07-08' → valid return trip
yyyy-mm-dd) sort lexically, a plain ret < depart comparison works — no Date parsing needed.You'll build a form where one control reshapes another — the return field switches on and off with the trip type, and the Book button reflects a rule that spans two fields.
There are three controls: a trip-type dropdown and two dates. They're not independent. Picking "one-way" makes the return date meaningless, so it should be disabled. Picking "return" turns it on, but now a new rule applies: you can't return before you depart, so the Book button has to refuse an invalid pair. The skill is letting these relationships fall out of a little derived state rather than scattering enable/disable flags around.
Keep only the raw inputs in state: type, depart, and ret. Everything else — whether the return field is enabled, whether the dates are valid, whether Book is allowed — is derived from those three on each render. ISO date strings compare correctly with <, so the validation is a one-liner. State holds the facts; the UI's enabled/disabled/invalid states are computed views of them.
A first pass often leaves both dates always editable and skips the cross-field rule:
export default function App() {
const [type, setType] = useState('one-way');
const [depart, setDepart] = useState('');
const [ret, setRet] = useState('');
return (
<>
<select value={type} onChange={(e) => setType(e.target.value)}>…</select>
<input type="date" value={depart} onChange={(e) => setDepart(e.target.value)} />
<input type="date" value={ret} onChange={(e) => setRet(e.target.value)} />
<button onClick={book}>Book</button> {/* always enabled */}
</>
);
}
This "works" but lets the user book nonsense: a return date is editable even for a one-way trip, and you can book a return flight that comes home before it leaves. The fields look independent, but the spec says they aren't — the missing piece is derived enable/validate logic tying them together.
import { useState } from 'react';
import './styles.css';
export default function App() {
const [type, setType] = useState('one-way');
const [depart, setDepart] = useState('2026-07-01');
const [ret, setRet] = useState('2026-07-08');
const [booked, setBooked] = useState(null);
// Derived from the inputs — nothing stored.
const isReturn = type === 'return';
const datesInvalid = isReturn && ret < depart; // ISO strings compare correctly
const canBook = depart !== '' && (!isReturn || (ret !== '' && !datesInvalid));
function book() {
setBooked(
isReturn
? `Booked a return flight departing ${depart}, returning ${ret}.`
: `Booked a one-way flight on ${depart}.`,
);
}
if (booked) {
return (
<main className="container">
<h1>Flight Booker</h1>
<div className="booked">
<h2>✈ Confirmed</h2>
<p>{booked}</p>
<button onClick={() => setBooked(null)}>Book another</button>
</div>
</main>
);
}
return (
<main className="container">
<h1>Flight Booker</h1>
<div className="flight-booker">
<select value={type} onChange={(e) => setType(e.target.value)}>
<option value="one-way">One-way flight</option>
<option value="return">Return flight</option>
</select>
<input type="date" value={depart} onChange={(e) => setDepart(e.target.value)} />
<input
type="date"
value={ret}
disabled={!isReturn}
className={datesInvalid ? 'invalid' : ''}
onChange={(e) => setRet(e.target.value)}
/>
<button className="book" type="button" disabled={!canBook} onClick={book}>
Book
</button>
</div>
</main>
);
}
The shifts: isReturn, datesInvalid, and canBook are computed each render from the three inputs, so the return field's disabled, its invalid class, and the Book button's disabled all stay correct automatically. The cross-field rule is the single expression ret < depart, made trivial by ISO date strings sorting lexically.
Start at type = 'one-way', depart = '2026-07-01', ret = '2026-07-08'.
isReturn is false, so the return input is disabled; datesInvalid is false; canBook is depart !== '' → true. Book is enabled.setType('return') re-renders. Now isReturn is true, the return input enables, and canBook checks the dates: '2026-07-08' < '2026-07-01' is false, so still valid — Book stays enabled.2026-06-20. datesInvalid becomes '2026-06-20' < '2026-07-01' → true. The return field gets the invalid class (red border) and canBook is false — Book disables.2026-07-10. datesInvalid flips back to false; Book re-enables. Clicking it sets booked to the return-trip message and the confirmation view renders.Every enable/disable/invalid state recomputed itself from type/depart/ret — no manual flag juggling.
disabled={!isReturn}.datesInvalid and gate the Book button on it.new Date(a) > new Date(b) works but is overkill. Fix: ISO yyyy-mm-dd strings compare directly with <.canBook/isValid in state. It drifts from the inputs. Fix: derive it during render.min to today so past dates can't be chosen, and the return's min to the departure date.This version groups the raw form values and confirmation into one state record. All enabled, invalid, and bookable values remain derived, and the rendered UI is unchanged.
import { useState } from 'react';
import './styles.css';
type BookingState = { type: string; depart: string; ret: string; booked: string | null };
export default function App() {
const [state, setState] = useState<BookingState>({ type: 'one-way', depart: '2026-07-01', ret: '2026-07-08', booked: null });
const update = (patch: Partial<BookingState>) => setState((current) => ({ ...current, ...patch }));
const isReturn = state.type === 'return';
const datesInvalid = isReturn && state.ret < state.depart;
const canBook = state.depart !== '' && (!isReturn || (state.ret !== '' && !datesInvalid));
function book() {
update({ booked: isReturn
? `Booked a return flight departing ${state.depart}, returning ${state.ret}.`
: `Booked a one-way flight on ${state.depart}.` });
}
return (
<main className="container">
<h1>Flight Booker</h1>
{state.booked ? (
<div className="booked"><h2>✈ Confirmed</h2><p>{state.booked}</p><button onClick={() => update({ booked: null })}>Book another</button></div>
) : (
<div className="flight-booker">
<select value={state.type} onChange={(e) => update({ type: e.target.value })}>
<option value="one-way">One-way flight</option><option value="return">Return flight</option>
</select>
<input type="date" value={state.depart} onChange={(e) => update({ depart: e.target.value })} />
<input type="date" value={state.ret} disabled={!isReturn} className={datesInvalid ? 'invalid' : ''} onChange={(e) => update({ ret: e.target.value })} />
<button className="book" type="button" disabled={!canBook} onClick={book}>Book</button>
</div>
)}
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.