State Management Patterns
State Management Patterns
Best practices for managing state with FullEventCalendar in different React application architectures.
Overview
FullEventCalendar can be integrated with various state management approaches:
- Built-in Hook: Using the included
useEventCalendar
hook (simplest approach) - React Context API: For sharing calendar state across components
- Redux/Zustand/Jotai: For integrating with global state management libraries
- Server Components + Client Islands: For Next.js App Router architecture
Built-in Hook Approach
The simplest approach is to use the built-in useEventCalendar
hook:
import { EventCalendar } from '@/components/event-calendar';
import { useEventCalendar } from '@/components/event-calendar/hooks/useEventCalendar';
const MyCalendarPage = () => {
const {
events,
isLoading,
addEvent,
updateEvent,
deleteEvent,
onViewOrDateChange
} = useEventCalendar({
initialEvents: [],
onEventAdd: async (event) => {
// API integration code
return createdEvent;
},
onEventUpdate: async (event) => {
// API integration code
return updatedEvent;
},
onEventDelete: async (eventId) => {
// API integration code
},
onDateRangeChange: async (startDate, endDate) => {
// API integration code
return fetchedEvents;
}
});
return (
<EventCalendar
events={events}
isLoading={isLoading}
onEventAdd={addEvent}
onEventUpdate={updateEvent}
onEventDelete={deleteEvent}
onDateRangeChange={onViewOrDateChange}
config={{
defaultView: 'month',
use24HourFormatByDefault: true
}}
/>
);
};
When to Use
- Single calendar instance in your application
- Simple projects with limited state sharing needs
- Prototyping and quick implementations
React Context Pattern
For sharing calendar state across multiple components:
// CalendarContext.tsx
import React, { createContext, useContext, ReactNode } from 'react';
import { useEventCalendar } from '@/components/event-calendar/hooks/useEventCalendar';
import { CalendarEventType, EventCalendarConfigType } from '@/components/event-calendar/types';
interface CalendarContextProps {
events: CalendarEventType[];
isLoading: boolean;
addEvent: (event: Omit<CalendarEventType, 'id'>) => Promise<void>;
updateEvent: (event: CalendarEventType) => Promise<void>;
deleteEvent: (eventId: string) => Promise<void>;
onViewOrDateChange: (startDate: Date, endDate: Date) => Promise<void>;
config: EventCalendarConfigType;
}
const CalendarContext = createContext<CalendarContextProps | undefined>(undefined);
export const CalendarProvider: React.FC<{
children: ReactNode;
config: EventCalendarConfigType;
}> = ({ children, config }) => {
const calendarState = useEventCalendar({
config,
initialEvents: [],
onEventAdd: async (event) => {
// API integration code
return createdEvent;
},
onEventUpdate: async (event) => {
// API integration code
return updatedEvent;
},
onEventDelete: async (eventId) => {
// API integration code
},
onDateRangeChange: async (startDate, endDate) => {
// API integration code
return fetchedEvents;
}
});
return (
<CalendarContext.Provider
value={{
...calendarState,
config
}}
>
{children}
</CalendarContext.Provider>
);
};
export const useCalendar = () => {
const context = useContext(CalendarContext);
if (context === undefined) {
throw new Error('useCalendar must be used within a CalendarProvider');
}
return context;
};
Usage:
// App.tsx
import { CalendarProvider } from './CalendarContext';
const App = () => {
const config = {
defaultView: 'month',
use24HourFormatByDefault: true
};
return (
<CalendarProvider config={config}>
<CalendarPage />
<EventListSidebar />
</CalendarProvider>
);
};
// CalendarPage.tsx
import { EventCalendar } from '@/components/event-calendar';
import { useCalendar } from './CalendarContext';
const CalendarPage = () => {
const {
events,
isLoading,
addEvent,
updateEvent,
deleteEvent,
onViewOrDateChange,
config
} = useCalendar();
return (
<EventCalendar
events={events}
isLoading={isLoading}
onEventAdd={addEvent}
onEventUpdate={updateEvent}
onEventDelete={deleteEvent}
onDateRangeChange={onViewOrDateChange}
config={config}
/>
);
};
// EventListSidebar.tsx
import { useCalendar } from './CalendarContext';
const EventListSidebar = () => {
const { events } = useCalendar();
return (
<div className="sidebar">
<h2>Upcoming Events</h2>
<ul>
{events.map(event => (
<li key={event.id}>
{event.title} - {event.startDate.toLocaleDateString()}
</li>
))}
</ul>
</div>
);
};
When to Use
- Sharing calendar state across multiple components
- Building custom calendar UIs or features
- Medium-sized applications with moderate state sharing needs
Redux Integration
For applications using Redux:
// calendarSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { CalendarEventType } from '@/components/event-calendar/types';
interface CalendarState {
events: CalendarEventType[];
isLoading: boolean;
error: string | null;
}
const initialState: CalendarState = {
events: [],
isLoading: false,
error: null
};
export const fetchEvents = createAsyncThunk(
'calendar/fetchEvents',
async ({ startDate, endDate }: { startDate: Date; endDate: Date }) => {
const response = await fetch(
`/api/events?start=${startDate.toISOString()}&end=${endDate.toISOString()}`
);
if (!response.ok) {
throw new Error('Failed to fetch events');
}
return await response.json();
}
);
export const addEvent = createAsyncThunk(
'calendar/addEvent',
async (event: Omit<CalendarEventType, 'id'>) => {
const response = await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
if (!response.ok) {
throw new Error('Failed to add event');
}
return await response.json();
}
);
export const updateEvent = createAsyncThunk(
'calendar/updateEvent',
async (event: CalendarEventType) => {
const response = await fetch(`/api/events/${event.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
if (!response.ok) {
throw new Error('Failed to update event');
}
return event;
}
);
export const deleteEvent = createAsyncThunk(
'calendar/deleteEvent',
async (eventId: string) => {
const response = await fetch(`/api/events/${eventId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete event');
}
return eventId;
}
);
const calendarSlice = createSlice({
name: 'calendar',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
// Fetch events
.addCase(fetchEvents.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchEvents.fulfilled, (state, action) => {
state.events = action.payload;
state.isLoading = false;
})
.addCase(fetchEvents.rejected, (state, action) => {
state.isLoading = false;
state.error = action.error.message || 'Failed to fetch events';
})
// Add event
.addCase(addEvent.fulfilled, (state, action) => {
state.events.push(action.payload);
})
// Update event
.addCase(updateEvent.fulfilled, (state, action) => {
const index = state.events.findIndex(event => event.id === action.payload.id);
if (index !== -1) {
state.events[index] = action.payload;
}
})
// Delete event
.addCase(deleteEvent.fulfilled, (state, action) => {
state.events = state.events.filter(event => event.id !== action.payload);
});
}
});
export default calendarSlice.reducer;
Usage with Redux:
// CalendarPage.tsx
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { EventCalendar } from '@/components/event-calendar';
import {
fetchEvents,
addEvent,
updateEvent,
deleteEvent
} from './calendarSlice';
import { RootState, AppDispatch } from './store';
const CalendarPage = () => {
const dispatch = useDispatch<AppDispatch>();
const { events, isLoading } = useSelector((state: RootState) => state.calendar);
const handleEventAdd = async (event: Omit<CalendarEventType, 'id'>) => {
try {
const resultAction = await dispatch(addEvent(event));
return resultAction.payload;
} catch (error) {
console.error('Failed to add event:', error);
throw error;
}
};
const handleEventUpdate = async (event: CalendarEventType) => {
try {
await dispatch(updateEvent(event));
return event;
} catch (error) {
console.error('Failed to update event:', error);
throw error;
}
};
const handleEventDelete = async (eventId: string) => {
try {
await dispatch(deleteEvent(eventId));
} catch (error) {
console.error('Failed to delete event:', error);
throw error;
}
};
const handleDateRangeChange = async (startDate: Date, endDate: Date) => {
try {
await dispatch(fetchEvents({ startDate, endDate }));
} catch (error) {
console.error('Failed to fetch events:', error);
throw error;
}
};
return (
<EventCalendar
events={events}
isLoading={isLoading}
onEventAdd={handleEventAdd}
onEventUpdate={handleEventUpdate}
onEventDelete={handleEventDelete}
onDateRangeChange={handleDateRangeChange}
config={{
defaultView: 'month',
use24HourFormatByDefault: true
}}
/>
);
};
When to Use
- Large applications with complex state management needs
- Applications already using Redux
- When you need advanced features like time-travel debugging
- When calendar state needs to be part of your app's global state
Next.js App Router (Server Components + Client Islands)
For Next.js applications using the App Router:
// app/calendar/page.tsx (Server Component)
import { Suspense } from 'react';
import { CalendarClientWrapper } from './calendar-client';
import { getEvents } from '@/lib/api';
export default async function CalendarPage() {
// Fetch initial events on the server
const initialEvents = await getEvents(
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 1 week ago
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days ahead
);
return (
<div className="container mx-auto py-8">
<h1 className="text-2xl font-bold mb-6">Calendar</h1>
<Suspense fallback={<div>Loading calendar...</div>}>
<CalendarClientWrapper initialEvents={initialEvents} />
</Suspense>
</div>
);
}
// app/calendar/calendar-client.tsx (Client Component)
'use client';
import { EventCalendar } from '@/components/event-calendar';
import { useEventCalendar } from '@/components/event-calendar/hooks/useEventCalendar';
import { CalendarEventType } from '@/components/event-calendar/types';
export function CalendarClientWrapper({
initialEvents
}: {
initialEvents: CalendarEventType[]
}) {
const {
events,
isLoading,
addEvent,
updateEvent,
deleteEvent,
onViewOrDateChange
} = useEventCalendar({
initialEvents,
onEventAdd: async (event) => {
const response = await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
if (!response.ok) {
throw new Error('Failed to create event');
}
return await response.json();
},
onEventUpdate: async (event) => {
const response = await fetch(`/api/events/${event.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
if (!response.ok) {
throw new Error('Failed to update event');
}
return event;
},
onEventDelete: async (eventId) => {
const response = await fetch(`/api/events/${eventId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete event');
}
},
onDateRangeChange: async (startDate, endDate) => {
const response = await fetch(
`/api/events?start=${startDate.toISOString()}&end=${endDate.toISOString()}`
);
if (!response.ok) {
throw new Error('Failed to fetch events');
}
return await response.json();
}
});
return (
<EventCalendar
events={events}
isLoading={isLoading}
onEventAdd={addEvent}
onEventUpdate={updateEvent}
onEventDelete={deleteEvent}
onDateRangeChange={onViewOrDateChange}
config={{
defaultView: 'month',
use24HourFormatByDefault: true
}}
/>
);
}
When to Use
- Next.js applications using the App Router
- When you want to leverage server components for initial data fetching
- When you need SEO benefits of server rendering
Choosing the Right Approach
Consider these factors when choosing a state management approach:
- App Size: For smaller apps, use the built-in hook. For larger apps, consider Redux, Zustand, or React Query.
- Team Familiarity: Stick with what your team knows best.
- Future Growth: Choose an approach that can scale with your application.
- Performance Needs: For high-performance requirements, consider more advanced approaches with optimizations.
- Server Rendering: For Next.js, consider the App Router pattern for better SEO and performance.
Performance Optimization Strategies
Regardless of which state management approach you choose, consider these optimization strategies:
-
Memoization: Use React.memo and useMemo to prevent unnecessary re-renders.
const MemoizedEventCalendar = React.memo(EventCalendar); const CalendarPage = () => { // State and handlers // Memoize event handlers to prevent re-renders const handleEventAdd = useCallback(async (event) => { // Implementation }, []); return ( <MemoizedEventCalendar events={events} onEventAdd={handleEventAdd} // Other props /> ); };
-
Lazy Loading: Only load events for the visible date range.
-
Pagination: For list views with many events.
-
Virtualization: For rendering large lists efficiently.
-
Optimistic Updates: Update the UI immediately while API calls happen in the background.
Conclusion
FullEventCalendar is flexible enough to work with any state management approach. While the built-in useEventCalendar
hook is sufficient for many applications, you can integrate the calendar with more advanced state management patterns as your application's needs grow.
Start with the simplest approach that meets your requirements, and refactor to more complex patterns only when needed.