Integrations

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:

  1. Built-in Hook: Using the included useEventCalendar hook (simplest approach)
  2. React Context API: For sharing calendar state across components
  3. Redux/Zustand/Jotai: For integrating with global state management libraries
  4. 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:

  1. 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 /> ); };
  2. Lazy Loading: Only load events for the visible date range.

  3. Pagination: For list views with many events.

  4. Virtualization: For rendering large lists efficiently.

  5. 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.