Skip to main content

๐Ÿ’ก Calendar Component - Best Practices & Tips

Kumpulan best practices, common patterns, dan tips untuk memaksimalkan penggunaan Calendar Component.


๐ŸŽฏ Architecture Patternsโ€‹

1. Separation of Concernsโ€‹

// โœ… Good: Pisahkan API logic, business logic, dan UI

// services/eventService.ts - API Layer
export const eventService = {
getEvents: async (start: Date, end: Date) => {
const response = await axios.get('/api/events', {
params: { start, end }
});
return response.data;
},
};

// hooks/useCalendarEvents.ts - Business Logic Layer
export const useCalendarEvents = (filters?: EventFilters) => {
const fetchEvents = async (start: Date, end: Date) => {
const events = await eventService.getEvents(start, end);

// Apply business logic
return events.filter(event => {
if (filters?.status) {
return event.status === filters.status;
}
return true;
});
};

return { fetchEvents };
};

// components/Calendar/MyCalendar.tsx - UI Layer
export default function MyCalendar() {
const { fetchEvents } = useCalendarEvents();

return (
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
/>
);
}

2. Custom Hook Patternโ€‹

Buat custom hook untuk encapsulate calendar logic:

hooks/useCalendar.ts
import { useRef, useState } from 'react';
import { GenericCalendarRef } from '@/components/Calendar/GenericCalendar';
import { SlotInfo } from 'react-big-calendar';
import { eventService } from '@/services/eventService';

export const useCalendar = () => {
const calendarRef = useRef<GenericCalendarRef>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedSlot, setSelectedSlot] = useState<SlotInfo | null>(null);

const fetchEvents = async (start: Date, end: Date) => {
return eventService.getEvents(start, end);
};

const handleSelectSlot = (slotInfo: SlotInfo) => {
setSelectedSlot(slotInfo);
setIsModalOpen(true);
};

const handleCreateEvent = async (eventData: any) => {
await eventService.createEvent(eventData);
setIsModalOpen(false);
calendarRef.current?.refresh();
};

const refresh = () => {
calendarRef.current?.refresh();
};

return {
calendarRef,
isModalOpen,
selectedSlot,
fetchEvents,
handleSelectSlot,
handleCreateEvent,
setIsModalOpen,
refresh,
};
};

// Usage
function MyCalendar() {
const calendar = useCalendar();

return (
<>
<GenericOptimizedCalendar
ref={calendar.calendarRef}
onFetchEvents={calendar.fetchEvents}
onSelectSlot={calendar.handleSelectSlot}
/>
{calendar.isModalOpen && (
<Modal onClose={() => calendar.setIsModalOpen(false)}>
{/* Form */}
</Modal>
)}
</>
);
}

3. Context Pattern for Shared Stateโ€‹

context/CalendarContext.tsx
import { createContext, useContext, useState } from 'react';

interface CalendarContextValue {
filters: EventFilters;
setFilters: (filters: EventFilters) => void;
selectedDate: Date;
setSelectedDate: (date: Date) => void;
}

const CalendarContext = createContext<CalendarContextValue | null>(null);

export const CalendarProvider = ({ children }) => {
const [filters, setFilters] = useState<EventFilters>({});
const [selectedDate, setSelectedDate] = useState(new Date());

return (
<CalendarContext.Provider
value={{ filters, setFilters, selectedDate, setSelectedDate }}
>
{children}
</CalendarContext.Provider>
);
};

export const useCalendarContext = () => {
const context = useContext(CalendarContext);
if (!context) {
throw new Error('useCalendarContext must be used within CalendarProvider');
}
return context;
};

// Usage
function CalendarPage() {
return (
<CalendarProvider>
<CalendarFilters />
<CalendarView />
</CalendarProvider>
);
}

๐Ÿš€ Performance Optimizationโ€‹

1. Memoize Event Transformationsโ€‹

import { useMemo } from 'react';

function OptimizedCalendar() {
const fetchEvents = async (start: Date, end: Date) => {
const data = await eventService.getEvents(start, end);

// โŒ Bad: Transform di render
return data.map(e => ({
...e,
color: getColor(e.status),
disabled: isPast(e.start),
}));
};

// โœ… Good: Memoize transformation functions
const getEventColor = useMemo(() => {
return (event: Event) => {
switch (event.status) {
case 'approved': return '#4caf50';
case 'pending': return '#ff9800';
case 'rejected': return '#f44336';
default: return '#3174ad';
}
};
}, []);

const isEventDisabled = useMemo(() => {
return (event: Event) => {
return dayjs(event.start).isBefore(dayjs(), 'day');
};
}, []);

return (
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
getEventColor={getEventColor}
isEventDisabled={isEventDisabled}
/>
);
}

2. Optimize React Query Settingsโ€‹

const calendarQueryOptions = {
staleTimeMs: 5 * 60 * 1000, // 5 minutes
queryOptions: {
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnMount: false,
refetchOnWindowFocus: false,
},
};

<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
{...calendarQueryOptions}
/>

3. Lazy Load Heavy Componentsโ€‹

import { lazy, Suspense } from 'react';

const CalendarComponent = lazy(() =>
import('@/components/Calendar/GenericCalendar').then(mod => ({
default: mod.GenericOptimizedCalendar
}))
);

function CalendarPage() {
return (
<Suspense fallback={<div>Loading calendar...</div>}>
<CalendarComponent onFetchEvents={fetchEvents} />
</Suspense>
);
}

๐ŸŽจ Styling Best Practicesโ€‹

1. CSS-in-JS dengan Emotion/Styled-Componentsโ€‹

import styled from '@emotion/styled';

const CalendarContainer = styled.div`
height: calc(100vh - 120px);
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);

.rbc-calendar {
font-family: 'Inter', sans-serif;
}

.rbc-event {
border-radius: 6px;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s;

&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
}

.rbc-today {
background-color: rgba(33, 150, 243, 0.05);
}
`;

function StyledCalendar() {
return (
<CalendarContainer>
<GenericOptimizedCalendar onFetchEvents={fetchEvents} />
</CalendarContainer>
);
}

2. Tailwind CSS Integrationโ€‹

function TailwindCalendar() {
return (
<div className="h-screen p-4 bg-gray-50">
<div className="h-full bg-white rounded-lg shadow-lg p-6">
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
disabledSlotClassName="bg-gray-100 cursor-not-allowed opacity-50"
/>
</div>
</div>
);
}
globals.css
/* Custom RBC styling dengan Tailwind */
.rbc-calendar {
@apply font-sans;
}

.rbc-event {
@apply rounded-md border-none shadow-sm transition-all duration-200;
}

.rbc-event:hover {
@apply -translate-y-0.5 shadow-md;
}

.rbc-today {
@apply bg-blue-50;
}

.rbc-toolbar button {
@apply px-4 py-2 rounded-md border border-gray-300 bg-white text-gray-700 transition-colors;
}

.rbc-toolbar button:hover {
@apply bg-gray-50 border-gray-400;
}

.rbc-toolbar button.rbc-active {
@apply bg-blue-600 text-white border-blue-600;
}

๐Ÿ”’ Security Best Practicesโ€‹

1. Sanitize User Inputโ€‹

import DOMPurify from 'dompurify';

const handleCreateEvent = async (formData: EventFormData) => {
// Sanitize input
const sanitizedData = {
title: DOMPurify.sanitize(formData.title),
description: DOMPurify.sanitize(formData.description),
start: formData.start,
end: formData.end,
};

await eventService.createEvent(sanitizedData);
};

2. Validate Date Rangesโ€‹

const handleSelectSlot = (slotInfo: SlotInfo) => {
const { start, end } = slotInfo;

// Validate date range
if (dayjs(end).diff(start, 'hour') > 8) {
alert('Event tidak boleh lebih dari 8 jam');
return;
}

if (dayjs(start).isBefore(dayjs(), 'day')) {
alert('Tidak bisa membuat event di masa lalu');
return;
}

// Proceed
setSelectedSlot(slotInfo);
};

3. Permission-based Actionsโ€‹

interface User {
role: 'admin' | 'user' | 'viewer';
permissions: string[];
}

function PermissionBasedCalendar({ user }: { user: User }) {
const canCreate = user.permissions.includes('events:create');
const canEdit = user.permissions.includes('events:edit');
const canDelete = user.permissions.includes('events:delete');

const handleSelectSlot = (slotInfo: SlotInfo) => {
if (!canCreate) {
alert('Anda tidak memiliki permission untuk membuat event');
return;
}
// Proceed
};

const isEventDisabled = (event: Event) => {
return !canEdit || event.createdBy !== user.id;
};

return (
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
onSelectSlot={canCreate ? handleSelectSlot : undefined}
isEventDisabled={isEventDisabled}
/>
);
}

๐Ÿ“ฑ Mobile Optimizationโ€‹

1. Responsive Layoutโ€‹

import { useMediaQuery } from '@mantine/hooks';

function ResponsiveCalendar() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isTablet = useMediaQuery('(max-width: 1024px)');

return (
<div style={{
height: isMobile ? 'calc(100vh - 60px)' : 'calc(100vh - 120px)',
padding: isMobile ? '8px' : '20px',
}}>
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
defaultView={isMobile ? 'day' : 'month'}
components={{
toolbar: (props) => (
<CustomToolbar {...props} isMobile={isMobile} />
),
}}
/>
</div>
);
}

2. Touch-friendly Interactionsโ€‹

const calendarProps = {
// Increase touch target size
step: 30, // 30-minute slots
timeslots: 2, // 2 slots per step = 1 hour

// Disable selectable on mobile for better UX
selectable: !isMobile,

// Custom event style for mobile
style: {
fontSize: isMobile ? '12px' : '14px',
padding: isMobile ? '4px' : '8px',
},
};

๐Ÿงช Testing Patternsโ€‹

1. Test with Mock Dataโ€‹

__tests__/Calendar.test.tsx
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { GenericOptimizedCalendar } from '@/components/Calendar/GenericCalendar';

const mockEvents = [
{
id: '1',
title: 'Meeting',
start: new Date('2025-12-05T09:00:00'),
end: new Date('2025-12-05T10:00:00'),
color: '#3174ad',
},
{
id: '2',
title: 'Lunch',
start: new Date('2025-12-05T12:00:00'),
end: new Date('2025-12-05T13:00:00'),
color: '#4caf50',
},
];

describe('GenericOptimizedCalendar', () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});

const mockFetchEvents = jest.fn().mockResolvedValue(mockEvents);

const renderCalendar = (props = {}) => {
return render(
<QueryClientProvider client={queryClient}>
<div style={{ height: '600px' }}>
<GenericOptimizedCalendar
onFetchEvents={mockFetchEvents}
{...props}
/>
</div>
</QueryClientProvider>
);
};

beforeEach(() => {
jest.clearAllMocks();
});

it('fetches and displays events', async () => {
renderCalendar();

await waitFor(() => {
expect(mockFetchEvents).toHaveBeenCalled();
});

expect(screen.getByText('Meeting')).toBeInTheDocument();
expect(screen.getByText('Lunch')).toBeInTheDocument();
});

it('calls onSelectSlot when slot is clicked', async () => {
const onSelectSlot = jest.fn();
renderCalendar({ onSelectSlot });

await waitFor(() => {
expect(mockFetchEvents).toHaveBeenCalled();
});

// Simulate slot selection
const slot = screen.getAllByRole('cell')[15]; // arbitrary cell
fireEvent.click(slot);

expect(onSelectSlot).toHaveBeenCalled();
});

it('applies color to events', async () => {
const getEventColor = jest.fn((event) => event.color);
renderCalendar({ getEventColor });

await waitFor(() => {
expect(getEventColor).toHaveBeenCalledTimes(mockEvents.length);
});
});
});

2. Integration Test with APIโ€‹

import { setupServer } from 'msw/node';
import { rest } from 'msw';

const server = setupServer(
rest.get('/api/events', (req, res, ctx) => {
return res(
ctx.json([
{
id: '1',
title: 'API Event',
start: '2025-12-05T09:00:00',
end: '2025-12-05T10:00:00',
},
])
);
})
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

it('fetches events from API', async () => {
const fetchEvents = async (start: Date, end: Date) => {
const res = await fetch('/api/events');
return res.json();
};

renderCalendar({ onFetchEvents: fetchEvents });

await waitFor(() => {
expect(screen.getByText('API Event')).toBeInTheDocument();
});
});

๐Ÿ› Error Handlingโ€‹

1. Graceful Error Handlingโ€‹

function CalendarWithErrorHandling() {
const [error, setError] = useState<Error | null>(null);

const fetchEvents = async (start: Date, end: Date) => {
try {
return await eventService.getEvents(start, end);
} catch (err) {
setError(err as Error);
console.error('Failed to fetch events:', err);
return []; // Return empty array to prevent crash
}
};

const handleSelectSlotDenied = (reason: string) => {
const messages = {
'outside-working-days': 'Tidak bisa membuat event di hari libur',
'outside-time-window': 'Jam kerja hanya 08:00 - 17:00',
'outside-global-bounds': 'Tanggal tidak valid',
'overlap': 'Slot ini sudah terisi',
};

alert(messages[reason] || 'Tidak bisa membuat event');
};

if (error) {
return (
<div className="error-container">
<p>Gagal memuat calendar: {error.message}</p>
<button onClick={() => setError(null)}>Retry</button>
</div>
);
}

return (
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
onSelectSlotDenied={handleSelectSlotDenied}
renderLoading={<div>Loading calendar...</div>}
/>
);
}

2. Network Error Recoveryโ€‹

import { useQueryClient } from '@tanstack/react-query';

function CalendarWithRetry() {
const queryClient = useQueryClient();
const [retryCount, setRetryCount] = useState(0);

const fetchEvents = async (start: Date, end: Date) => {
try {
return await eventService.getEvents(start, end);
} catch (error) {
if (retryCount < 3) {
setRetryCount(prev => prev + 1);
// Retry after delay
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
return fetchEvents(start, end);
}
throw error;
}
};

const handleRetry = () => {
setRetryCount(0);
queryClient.invalidateQueries(['calendar-events']);
};

return (
<div>
{retryCount > 0 && (
<div className="retry-banner">
Mencoba koneksi... (Attempt {retryCount}/3)
</div>
)}
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
queryOptions={{
onError: () => {
if (retryCount >= 3) {
alert('Gagal memuat data. Silakan coba lagi.');
}
},
}}
/>
</div>
);
}

๐Ÿ“Š Analytics & Monitoringโ€‹

1. Track User Interactionsโ€‹

import analytics from '@/lib/analytics';

function CalendarWithAnalytics() {
const handleSelectSlot = (slotInfo: SlotInfo) => {
// Track event
analytics.track('calendar_slot_selected', {
start: slotInfo.start,
end: slotInfo.end,
view: slotInfo.action, // 'select' | 'click' | 'doubleClick'
});

// Continue with normal flow
setSelectedSlot(slotInfo);
};

const handleSelectEvent = (event: Event) => {
analytics.track('calendar_event_clicked', {
eventId: event.id,
eventTitle: event.title,
});

setSelectedEvent(event);
};

return (
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
onSelectSlot={handleSelectSlot}
onSelectEventSimple={handleSelectEvent}
/>
);
}

2. Performance Monitoringโ€‹

import { useEffect } from 'react';

function CalendarWithPerformance() {
useEffect(() => {
// Measure initial load time
const startTime = performance.now();

return () => {
const loadTime = performance.now() - startTime;
console.log(`Calendar loaded in ${loadTime}ms`);

// Send to monitoring service
analytics.timing('calendar_load_time', loadTime);
};
}, []);

const fetchEvents = async (start: Date, end: Date) => {
const fetchStart = performance.now();

try {
const events = await eventService.getEvents(start, end);

const fetchDuration = performance.now() - fetchStart;
analytics.timing('calendar_fetch_events', fetchDuration);

return events;
} catch (error) {
analytics.exception('calendar_fetch_failed', error);
throw error;
}
};

return (
<GenericOptimizedCalendar onFetchEvents={fetchEvents} />
);
}

๐ŸŽ“ Advanced Tipsโ€‹

1. Multi-tenant Calendarโ€‹

interface TenantConfig {
tenantId: string;
timeWindow: { start: string; end: string };
workingDays: number[];
colors: Record<string, string>;
}

function MultiTenantCalendar({ tenantConfig }: { tenantConfig: TenantConfig }) {
const fetchEvents = async (start: Date, end: Date) => {
return eventService.getEvents(start, end, {
tenantId: tenantConfig.tenantId,
});
};

const getEventColor = (event: Event) => {
return tenantConfig.colors[event.type] || '#3174ad';
};

return (
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
timeWindow={tenantConfig.timeWindow}
workingDays={tenantConfig.workingDays}
getEventColor={getEventColor}
queryParams={{ tenantId: tenantConfig.tenantId }}
/>
);
}

2. Calendar with Drag & Drop (Future Enhancement)โ€‹

// Note: GenericCalendar belum support DnD out-of-the-box
// Ini adalah example pattern untuk implement DnD

import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

function DraggableCalendar() {
const handleEventDrop = async ({ event, start, end }: any) => {
// Update event di backend
await eventService.updateEvent(event.id, {
start: start.toISOString(),
end: end.toISOString(),
});

// Refresh calendar
calendarRef.current?.refresh();
};

return (
<DndProvider backend={HTML5Backend}>
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
// Note: Requires custom implementation
// onEventDrop={handleEventDrop}
// draggableAccessor={() => true}
/>
</DndProvider>
);
}

โœ… Checklistโ€‹

Sebelum deploy ke production:

  • Setup React Query dengan proper config
  • Implement error handling
  • Add loading states
  • Optimize untuk mobile
  • Add analytics tracking
  • Write tests untuk critical flows
  • Setup proper TypeScript types
  • Validate date/time inputs
  • Implement permission checks
  • Test dengan real API data
  • Review performance (React DevTools Profiler)
  • Check accessibility (keyboard navigation, screen readers)
  • Test di berbagai browsers
  • Review security (XSS, injection)
  • Document API integration points

๐ŸŽฏ Summaryโ€‹

Key Takeaways:

  1. โœ… Separate concerns - API, business logic, UI
  2. โœ… Use custom hooks - Encapsulate reusable logic
  3. โœ… Optimize performance - Memoize, lazy load, cache
  4. โœ… Handle errors gracefully - Don't let calendar crash
  5. โœ… Make it responsive - Mobile-first approach
  6. โœ… Test thoroughly - Unit tests, integration tests
  7. โœ… Monitor performance - Track metrics & user behavior
  8. โœ… Security first - Validate & sanitize inputs

Happy Coding! ๐Ÿš€

Jika ada pertanyaan atau butuh pattern untuk use case spesifik, silakan review source code atau buat custom implementation berdasarkan best practices di atas.