How would you implement a calendar component with event scheduling in React?
Advertisement
🧩 Scenario
🧠 Architecture Walkthrough
The Event Data Model Drives Everything Downstream
Every architectural decision in a calendar system flows from how you model time in your event objects. Storing start and end as ISO strings is the right choice because ISO 8601 strings are serializable, sortable as strings, and unambiguous when they include timezone offset information.
The critical discipline is storing all times in UTC internally and converting to local time only at the rendering layer. If you store times in the user's local timezone and they travel or change their system timezone, all their events shift.
The moveEvent function in the demo shows this correctly: it preserves duration by computing new Date(ev.end) - new Date(ev.start) as a millisecond delta, then adds that delta to the new start time.
This works because Date subtraction always produces millisecond differences in UTC, so the duration calculation is timezone-agnostic. A team that reaches for moment.js or date-fns for this kind of math is making a reasonable trade: more readable code at the cost of bundle size.
Separating the Rendering Engine from the Interaction Model
The day view in this demo maps 24 hours to a list of time slots, then places events into the slot matching their start time. This is a simplified layout strategy that works for the demo but breaks for overlapping events.
A production day view needs a layout engine that takes all events for a day and assigns each one a column and columnCount events that overlap get placed side by side, each occupying 1/columnCount of the available width.
The rendering engine is a pure function: given an array of events and the pixel-per-minute ratio, it returns layout objects with top, height, left, and width for each event. Keeping this as a pure function means you can test it in isolation with just time data, without mounting any React components. Separating layout computation from React rendering is what makes calendar views maintainable as the event density grows.
Click-to-Create and Drag-to-Move as Pointer Event State Machines
Interaction in a calendar view is best modelled as a state machine with discrete modes: idle, creating, moving, and resizing. In idle mode, a click on an empty time slot creates an event at that time.
In creating mode, dragging extends the new event's end time. In moving mode, a drag on an existing event updates its start and end while preserving duration. In resizing mode, a drag on the bottom edge of an event updates only its end time.
The demo implements the click-to-create path via handleTimeSlotClick, which calls prompt() for the title this is fine for a demo but in production you would open a popover or modal anchored to the clicked slot.
The key implementation detail in createEvent is that the default duration is one hour, computed by adding 60 * 60 * 1000 milliseconds to the start time and converting back to ISO. This keeps all duration math in milliseconds, never in string manipulation.
💡 Key Code Explained
const moveEvent = (id, newStart) => {
setEvents((prev) =>
prev.map((ev) =>
ev.id === id
? {
...ev,
start: newStart,
end: new Date(
new Date(newStart).getTime() +
(new Date(ev.end) - new Date(ev.start)),
).toISOString(),
}
: ev,
),
);
};
This function is the core of drag-and-drop event movement. The duration is computed as new Date(ev.end) - new Date(ev.start), which yields a millisecond number this subtraction works because JavaScript coerces Date objects to their UTC millisecond timestamp in arithmetic contexts.
Adding that number to new Date(newStart).getTime() and wrapping in new Date(...).toISOString() produces a new end time that is exactly the original duration after the new start, regardless of timezone.
If you instead tried to compute duration by string-parsing hours and minutes, you would introduce bugs around midnight (where hours wrap from 23 to 0), DST transitions (where an hour disappears or appears), and leap seconds. The millisecond approach sidesteps all of these because it operates in linear UTC time.
const getEventsForDate = (date) => {
return events
.filter((event) => event.start.startsWith(date))
.sort((a, b) => new Date(a.start) - new Date(b.start));
};
This filter is intentionally simple for the demo context. startsWith(date) works when date is a local date string like '2025-11-23' and all events are stored in local time ISO format. The .sort() call is important without it, events would render in insertion order, which breaks the visual timeline.
The sort uses the same millisecond subtraction trick: new Date(a.start) - new Date(b.start) returns a negative number if a comes before b, which is exactly what Array.sort expects for ascending order.
In a production system you would replace startsWith with a proper interval intersection check an event that starts on November 22 at 23:00 and ends November 23 at 01:00 has occurrences on both days but startsWith('2025-11-23') would exclude it entirely.
⚖️ Tradeoffs
| Approach | Pro | Con |
|---|---|---|
| ISO strings stored in UTC (chosen) | Timezone-safe, serializable, sorts correctly as strings | Requires explicit conversion at display layer; easy to accidentally store in local time |
| Unix timestamps (numbers) | Compact, arithmetic is trivial | Not human-readable in the database, no timezone signal |
| Date objects in state | Convenient for math | Not serializable, causes issues with React's equality checks and persistence layers |
🎯 What Interviewers Actually Check
- Explains the UTC-internally / local-time-at-display discipline rather than assuming ISO strings are always timezone-correct
- Describes duration preservation in
moveEventusing millisecond arithmetic rather than string manipulation - Notes that
startsWithfiltering breaks for multi-day events and proposes interval intersection as the correct alternative - Can articulate the layout engine concept column assignment for overlapping events even if not implementing it in the demo
- Mentions that recurring events must be expanded only for the visible range, not expanded infinitely upfront
❓ Follow-Up Questions
- A user in New York creates a 9am event. A user in London views the same calendar what does it display, and what does your data model need to support this correctly?
- You need to detect conflicts between events and highlight them in red. Write the intersection test and explain where in the component tree you would run it.
- How would you implement drag-to-resize the end time of an event using pointer events, and how do you snap to 15-minute increments?
- Your calendar needs to support recurring events (every Monday for 8 weeks). How do you store the recurrence rule, and how do you avoid expanding it into 8 individual events in the database?
- Your PM wants the calendar to load events lazily as the user navigates months how do you structure the fetch layer to avoid re-fetching already-loaded months?
🎮 Live Demo
📝 Summary
A production calendar is built around three disciplines: storing all times in UTC with timezone-aware ISO strings, keeping layout computation as a pure function separate from React rendering, and modelling all user interactions as operations against the event array rather than as direct DOM mutations.
Duration preservation in move operations must use millisecond arithmetic, not string manipulation, to stay correct across DST boundaries and midnight crossings. The startsWith filter in the demo is a pragmatic simplification that works for same-timezone single-day events but must be replaced with proper interval intersection logic before the calendar can support multi-day events or users in different timezones.
Getting these foundations right is what separates a calendar that works in a single timezone demo from one that handles the full complexity of a scheduling tool used across a real user base.
Should the calendar compute recurring events on the client or server?
For complex recurrences (RRULE), do it on the server. Client-only is fine for simple repeat patterns.
How do you prevent event overlap?
Maintain normalized event ranges and detect conflicts using time window intersection checks.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement