๐ก 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:
- โ Separate concerns - API, business logic, UI
- โ Use custom hooks - Encapsulate reusable logic
- โ Optimize performance - Memoize, lazy load, cache
- โ Handle errors gracefully - Don't let calendar crash
- โ Make it responsive - Mobile-first approach
- โ Test thoroughly - Unit tests, integration tests
- โ Monitor performance - Track metrics & user behavior
- โ 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.