Architecture
Understanding the architecture and design patterns of React Event Calendar.
Overview
React Event Calendar is built with a modern architecture using Next.js App Router, Zustand for state management, URL state management with nuqs, and PostgreSQL with Drizzle ORM for data persistence. The calendar supports multiple views (day, week, month, year) with customizable configurations.
State Management
The calendar uses a combination of Zustand for global state management and URL state management with nuqs to enable shareable calendar views.
URL State with nuqs
URL parameters are managed with nuqs to enable shareable calendar views and bookmarking:
1import { CalendarViewType } from '@/types/event';
2import {
3 createSearchParamsCache,
4 parseAsArrayOf,
5 parseAsBoolean,
6 parseAsInteger,
7 parseAsIsoDate,
8 parseAsString,
9} from 'nuqs/server';
10
11export const searchParamsCache = createSearchParamsCache({
12 date: parseAsIsoDate.withDefault(new Date()),
13 view: parseAsString.withDefault(CalendarViewType.MONTH),
14 title: parseAsString.withDefault(''),
15 categories: parseAsArrayOf(parseAsString).withDefault([]),
16 daysCount: parseAsInteger.withDefault(7),
17 search: parseAsString.withDefault(''),
18 colors: parseAsArrayOf(parseAsString).withDefault([]),
19 locations: parseAsArrayOf(parseAsString).withDefault([]),
20 repeatingTypes: parseAsArrayOf(parseAsString).withDefault([]),
21 isRepeating: parseAsBoolean.withDefault(false),
22 limit: parseAsInteger.withDefault(50),
23 offset: parseAsInteger.withDefault(0),
24});
25
Zustand Store
The core state is managed by a Zustand store that handles view configurations, selected events, and UI states:
1import { create } from 'zustand';
2import { persist } from 'zustand/middleware';
3import {
4 CalendarViewType,
5 TimeFormatType,
6 ViewModeType,
7} from '@/types/event';
8
9const DEFAULT_VIEW_CONFIGS = {
10 day: {
11 showCurrentTimeIndicator: true,
12 showHoverTimeIndicator: true,
13 enableTimeSlotClick: true,
14 },
15 week: {
16 highlightToday: true,
17 showCurrentTimeIndicator: true,
18 showHoverTimeIndicator: true,
19 enableTimeSlotClick: true,
20 expandMultiDayEvents: true,
21 },
22 // ... other view configurations
23};
24
25export const useEventCalendarStore = create(
26 persist(
27 (set, get) => ({
28 selectedEvent: null,
29 currentView: CalendarViewType.DAY,
30 viewMode: ViewModeType.CALENDAR,
31 timeFormat: TimeFormatType.HOUR_24,
32 locale: 'en-US',
33 firstDayOfWeek: 0, // sunday
34 daysCount: 7,
35 viewSettings: DEFAULT_VIEW_CONFIGS,
36 // ... other state properties
37
38 setView: (view) => set({ currentView: view }),
39 setTimeFormat: (format) => set({ timeFormat: format }),
40 // ... other actions
41 }),
42 {
43 name: 'event-calendar',
44 partialize: (state) => ({
45 currentView: state.currentView,
46 viewMode: state.viewMode,
47 timeFormat: state.timeFormat,
48 locale: state.locale,
49 firstDayOfWeek: state.firstDayOfWeek,
50 daysCount: state.daysCount,
51 viewSettings: state.viewSettings,
52 }),
53 }
54 )
55);
Database Structure
The calendar uses PostgreSQL with Drizzle ORM for data persistence. The schema is designed to support various event properties including recurring events.
1import {
2 pgTable,
3 timestamp,
4 varchar,
5 uuid,
6 boolean,
7 text,
8} from 'drizzle-orm/pg-core';
9
10export const events = pgTable('events', {
11 id: uuid('id').primaryKey().defaultRandom(),
12 title: varchar('title', { length: 256 }).notNull(),
13 description: text('description').notNull(),
14 startDate: timestamp('start_date', { withTimezone: true }).notNull(),
15 endDate: timestamp('end_date', { withTimezone: true }).notNull(),
16 startTime: varchar('start_time', { length: 5 }).notNull(),
17 endTime: varchar('end_time', { length: 5 }).notNull(),
18 isRepeating: boolean('is_repeating').notNull(),
19 repeatingType: varchar('repeating_type', {
20 length: 10,
21 enum: ['daily', 'weekly', 'monthly'],
22 }).$type<'daily' | 'weekly' | 'monthly'>(),
23 location: varchar('location', { length: 256 }).notNull(),
24 category: varchar('category', { length: 100 }).notNull(),
25 color: varchar('color', { length: 15 }).notNull(),
26 createdAt: timestamp('created_at', { withTimezone: true })
27 .defaultNow()
28 .notNull(),
29 updatedAt: timestamp('updated_at', { withTimezone: true })
30 .defaultNow()
31 .notNull(),
32});
33
34export type EventTypes = typeof events.$inferSelect;
35export type newEvent = typeof events.$inferInsert;
Server Actions
Next.js Server Actions are used to interact with the database, providing type-safe data fetching and mutations:
1'use server';
2
3import { db } from '@/db';
4import { events } from '@/db/schema';
5import { CalendarViewType } from '@/types/event';
6import { and, between, eq, ilike, or, lte, gte } from 'drizzle-orm';
7import {
8 startOfDay,
9 endOfDay,
10 startOfWeek,
11 endOfWeek,
12 startOfMonth,
13 endOfMonth,
14 startOfYear,
15 endOfYear,
16} from 'date-fns';
17import { z } from 'zod';
18import { unstable_cache as cache, revalidatePath } from 'next/cache';
19
20const eventFilterSchema = z.object({
21 title: z.string().optional(),
22 categories: z.array(z.string()).default([]),
23 daysCount: z.number().optional(),
24 view: z.enum([
25 CalendarViewType.DAY,
26 CalendarViewType.DAYS,
27 CalendarViewType.WEEK,
28 CalendarViewType.MONTH,
29 CalendarViewType.YEAR,
30 ]).optional(),
31 date: z.date(),
32 // ... other filter properties
33});
34
35export type EventFilter = z.infer<typeof eventFilterSchema>;
36
37export const getEvents = cache(
38 async (filterParams: EventFilter) => {
39 try {
40 const filter = eventFilterSchema.parse(filterParams);
41
42 const currentDate = new Date(filter.date);
43 let dateRange: { start: Date; end: Date } = {
44 start: startOfMonth(currentDate),
45 end: endOfMonth(currentDate),
46 };
47
48 if (filter.view) {
49 switch (filter.view) {
50 case CalendarViewType.DAY:
51 dateRange = {
52 start: startOfDay(currentDate),
53 end: endOfDay(currentDate),
54 };
55 break;
56 case CalendarViewType.WEEK:
57 dateRange = {
58 start: startOfWeek(currentDate, { weekStartsOn: 0 }),
59 end: endOfWeek(currentDate, { weekStartsOn: 0 }),
60 };
61 break;
62 // ... other view cases
63 }
64 }
65
66 const conditions = [];
67
68 // Add date range condition
69 conditions.push(
70 or(
71 and(
72 between(events.startDate, dateRange.start, dateRange.end),
73 between(events.endDate, dateRange.start, dateRange.end),
74 ),
75 // ... other date conditions
76 ),
77 );
78
79 // Add other filter conditions
80 if (filter.title) {
81 conditions.push(ilike(events.title, `%${filter.title}%`));
82 }
83
84 if (filter.categories.length > 0) {
85 const categoryConditions = filter.categories.map((category) =>
86 eq(events.category, category),
87 );
88 conditions.push(or(...categoryConditions));
89 }
90
91 // Execute the query with all conditions
92 const result = await db
93 .select()
94 .from(events)
95 .where(and(...conditions))
96 .execute();
97
98 return {
99 events: result,
100 success: true,
101 timestamp: new Date().toISOString(),
102 };
103 } catch (error) {
104 console.error('Error fetching events:', error);
105 return {
106 events: [],
107 success: false,
108 error: error instanceof Error ? error.message : 'Error fetching events',
109 };
110 }
111 },
112 ['get-events'],
113 {
114 revalidate: 3600,
115 tags: ['events'],
116 }
117);
Component Architecture
The calendar is built with a modular component architecture that separates concerns:
Main Components
- EventCalendar: The main container component that manages state and renders the appropriate view
- CalendarHeader: Navigation controls, view switchers, and date display
- CalendarDayView: Single day detailed view with time slots
- CalendarWeekView: Week view showing multiple days with time slots
- CalendarMonthView: Traditional month grid view
- EventItem: Individual event rendering with positioning logic
- EventForm: Form for creating and editing events
1import { CalendarHeader } from './calendar-header';
2import { CalendarDayView } from './calendar-day-view';
3import { CalendarWeekView } from './calendar-week-view';
4import { CalendarMonthView } from './calendar-month-view';
5import { useEventCalendarStore } from '@/hooks/use-event-calendar';
6import { CalendarViewType } from '@/types/event';
7import { EventTypes } from '@/db/schema';
8
9interface EventCalendarProps {
10 events: EventTypes[];
11 initialDate: Date;
12}
13
14export function EventCalendar({ events, initialDate }: EventCalendarProps) {
15 const currentView = useEventCalendarStore((state) => state.currentView);
16
17 // Render different views based on the current view state
18 const renderCalendarView = () => {
19 switch (currentView) {
20 case CalendarViewType.DAY:
21 return <CalendarDayView events={events} date={initialDate} />;
22 case CalendarViewType.WEEK:
23 return <CalendarWeekView events={events} date={initialDate} />;
24 case CalendarViewType.MONTH:
25 return <CalendarMonthView events={events} date={initialDate} />;
26 default:
27 return <CalendarDayView events={events} date={initialDate} />;
28 }
29 };
30
31 return (
32 <div className="flex flex-col h-full">
33 <CalendarHeader />
34 {renderCalendarView()}
35 </div>
36 );
37}
Data Flow
The calendar follows a unidirectional data flow pattern with Next.js App Router and Server Actions:
- URL State: User interactions update URL parameters via nuqs
- Server Actions: URL parameters are parsed and used to fetch data from the database
- Component Rendering: Data is passed to components for rendering
- User Interactions: UI events trigger state updates and Server Actions
- Persistence: Changes are saved to the database via Server Actions and the UI is updated
1import { getEvents, getCategories } from './actions';
2import { searchParamsCache } from '@/lib/searchParams';
3import { CalendarViewType } from '@/types/event';
4import { EventCalendar } from '@/components/event-calendar/calendar';
5import { Suspense } from 'react';
6
7export default async function CalendarPage({ searchParams }) {
8 // Parse URL parameters using nuqs
9 const search = searchParamsCache.parse(searchParams);
10
11 // Fetch data using Server Actions
12 const eventsResponse = await getEvents({
13 date: search.date,
14 view: search.view as CalendarViewType,
15 daysCount: Number(search.daysCount),
16 categories: search.categories,
17 title: search.title,
18 colors: search.colors,
19 locations: search.locations,
20 isRepeating: search.isRepeating,
21 });
22
23 // Render the calendar with the fetched data
24 return (
25 <div className="container mx-auto p-4">
26 <Suspense fallback={<div>Loading calendar...</div>}>
27 <EventCalendar
28 events={eventsResponse.events}
29 initialDate={search.date}
30 />
31 </Suspense>
32 </div>
33 );
34}
Server-Side Rendering and Caching
The calendar uses Next.js Server Components and Server Actions to render content on the server and cache responses. The unstable_cache
API is used to cache event data for improved performance.
Event Filtering and Search
The calendar supports advanced filtering and search capabilities:
- Date Range Filtering: Events are filtered based on the selected view (day, week, month, year)
- Category Filtering: Events can be filtered by category
- Color Filtering: Events can be filtered by color
- Location Filtering: Events can be filtered by location
- Recurring Event Filtering: Events can be filtered by their recurring status
- Text Search: Events can be searched by title, description, or location
All filters can be combined to create complex queries, and the results are efficiently fetched from the database using Drizzle ORM's query builder.
Recurring Events
The calendar supports recurring events with different patterns:
- Daily: Events that repeat every day
- Weekly: Events that repeat on the same day every week
- Monthly: Events that repeat on the same day every month
Recurring events are stored in the database with a flag indicating their recurring status and the type of recurrence. The calendar logic handles the expansion of recurring events when displaying them in different views.
Performance Optimizations
The calendar implements several performance optimizations:
- Server-Side Rendering: Initial data is rendered on the server for faster page loads
- Data Caching: Server Actions use Next.js caching to reduce database queries
- Incremental Static Regeneration: Pages are statically generated and revalidated periodically
- Optimized Database Queries: Queries are optimized to fetch only the necessary data
- Component Memoization: React components use memoization to prevent unnecessary re-renders