Skip to content

React Components Skill for Happy Camper Planner

This skill provides specialized guidance for React component development in the Happy Camper Planner collaborative camping trip planning application.

When to Use This Skill

Use this skill when you need to: - Create or refactor React components and UI flows - Implement state management with useState hooks - Style components with Tailwind CSS patterns - Integrate Lucide React icons consistently - Build authentication and user management flows - Create collaborative features (plans, trips, gear, meals) - Implement RV compatibility and campground selection UIs - Handle complex form interactions and modal dialogs - Build responsive layouts for camping trip management - Create reusable UI patterns and component libraries

Technology Context

Frontend Stack

  • React 18 with functional components and hooks
  • Tailwind CSS for utility-first styling
  • Lucide React for consistent iconography
  • Firebase Authentication for user management
  • Vite as build tool and development server
  • Docker development environment on port 3000

Component Architecture

  • Large single-file components with embedded state and handlers
  • useState hooks for local component state management
  • Inline event handlers with clear state updates
  • Conditional rendering patterns with && and ternary operators
  • Mock data constants at component top for development

Component Development Patterns

Base Component Structure

import React, { useState, useEffect } from 'react';
import { 
  // Import specific Lucide icons
  Calendar, MapPin, Users, Plus, Settings, 
  Tent, Truck, CheckSquare, Utensils 
} from 'lucide-react';

const ComponentName = () => {
  // --- State Management ---
  const [isLoading, setIsLoading] = useState(false);
  const [activeTab, setActiveTab] = useState('dashboard');
  const [showModal, setShowModal] = useState(false);

  // --- Mock Data (Development) ---
  const [mockData] = useState([
    { id: "1", title: "Sample Data", /* ... */ },
    { id: "2", title: "More Data", /* ... */ }
  ]);

  // --- Effects ---
  useEffect(() => {
    // Component initialization
  }, []);

  // --- Event Handlers ---
  const handleAction = (itemId) => {
    setIsLoading(true);
    // Handle action
    setIsLoading(false);
  };

  // --- Render Methods ---
  const renderSection = () => {
    return (
      <div className="space-y-4">
        {/* Section content */}
      </div>
    );
  };

  return (
    <div className="min-h-screen bg-gray-50">
      {/* Main component content */}
    </div>
  );
};

export default ComponentName;

State Management Patterns

Local Component State

// Authentication state
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState(null);

// UI state
const [activeTab, setActiveTab] = useState('dashboard');
const [selectedTrip, setSelectedTrip] = useState(null);
const [showInviteModal, setShowInviteModal] = useState(false);
const [showWizard, setShowWizard] = useState(false);

// Data state with mock data for development
const [plans, setPlans] = useState([
  {
    id: "1",
    title: "2024 West Coast Tour",
    season: 2024,
    role: "Admin",
    members: [
      { id: "u1", name: "Alex T.", email: "alex.t@example.com", role: "Owner" },
      { id: "u2", name: "Sarah J.", email: "sarah.j@example.com", role: "Editor" }
    ],
    trips: [
      {
        id: "t1",
        campground: "Yosemite Valley",
        startDate: "2024-06-15",
        endDate: "2024-06-20",
        reservations: [
          { userId: "u1", siteNumber: "A23", confirmationCode: "YV2024-A23" }
        ]
      }
    ]
  }
]);

// Loading and error states
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

State Update Patterns

// Adding new items to collections
const addTrip = (planId, newTrip) => {
  setPlans(prev => prev.map(plan => 
    plan.id === planId 
      ? { ...plan, trips: [...plan.trips, { ...newTrip, id: generateId() }] }
      : plan
  ));
};

// Updating nested objects
const updateTripReservation = (tripId, reservationData) => {
  setPlans(prev => prev.map(plan => ({
    ...plan,
    trips: plan.trips.map(trip => 
      trip.id === tripId
        ? { ...trip, reservations: [...trip.reservations, reservationData] }
        : trip
    )
  })));
};

// Removing items with confirmation
const removePlanMember = (planId, userId) => {
  if (window.confirm('Remove member from plan?')) {
    setPlans(prev => prev.map(plan => 
      plan.id === planId
        ? { ...plan, members: plan.members.filter(m => m.id !== userId) }
        : plan
    ));
  }
};

Tailwind CSS Styling Patterns

Layout Patterns

// Main application layout
<div className="min-h-screen bg-gray-50">
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
    <div className="py-8">
      {/* Content */}
    </div>
  </div>
</div>

// Card layouts for camping content
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
  <div className="p-6">
    <div className="flex items-center justify-between mb-4">
      <h3 className="text-lg font-semibold text-gray-900">
        Plan Title
      </h3>
      <span className="text-sm text-gray-500">2024 Season</span>
    </div>
    {/* Card content */}
  </div>
</div>

// Responsive grid layouts
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  {items.map(item => (
    <div key={item.id} className="...">
      {/* Grid item */}
    </div>
  ))}
</div>

Interactive Elements

// Primary buttons for main actions
<button 
  onClick={handleCreatePlan}
  className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
  <Plus className="w-4 h-4 mr-2" />
  Create New Plan
</button>

// Secondary buttons for supporting actions
<button 
  onClick={handleInviteMembers}
  className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
  <Users className="w-4 h-4 mr-2" />
  Invite Members
</button>

// Tab navigation
<div className="border-b border-gray-200">
  <nav className="-mb-px flex space-x-8">
    {['dashboard', 'plans', 'profile'].map((tab) => (
      <button
        key={tab}
        onClick={() => setActiveTab(tab)}
        className={`py-2 px-1 border-b-2 font-medium text-sm ${
          activeTab === tab
            ? 'border-blue-500 text-blue-600'
            : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
        }`}
      >
        {tab.charAt(0).toUpperCase() + tab.slice(1)}
      </button>
    ))}
  </nav>
</div>

Form Patterns

// Form container with proper spacing
<form onSubmit={handleSubmit} className="space-y-6">
  <div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
    <div>
      <label htmlFor="planTitle" className="block text-sm font-medium text-gray-700">
        Plan Title
      </label>
      <input
        type="text"
        id="planTitle"
        name="planTitle"
        value={formData.planTitle}
        onChange={handleInputChange}
        className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
        placeholder="Enter plan title"
      />
    </div>

    <div>
      <label htmlFor="seasonYear" className="block text-sm font-medium text-gray-700">
        Season Year
      </label>
      <select
        id="seasonYear"
        name="seasonYear"
        value={formData.seasonYear}
        onChange={handleInputChange}
        className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
      >
        <option value="2024">2024</option>
        <option value="2025">2025</option>
        <option value="2026">2026</option>
      </select>
    </div>
  </div>
</form>

Lucide Icons Integration

Icon Usage Patterns

import { 
  // Navigation and UI
  Calendar, MapPin, Users, Plus, Settings, ChevronRight, ArrowLeft,
  // Camping specific
  Tent, Truck, Compass, Mountain, Trees,
  // Actions
  CheckSquare, Circle, CheckCircle2, X, Check,
  // Communication
  Mail, MessageSquare, Share2, Link as LinkIcon,
  // Status and feedback
  Clock, Info, Star, Activity, Zap, Sparkles, ShieldCheck
} from 'lucide-react';

// Icon with consistent sizing and colors
const IconButton = ({ icon: Icon, label, onClick, variant = 'primary' }) => (
  <button
    onClick={onClick}
    className={`inline-flex items-center px-3 py-2 rounded-md text-sm font-medium ${
      variant === 'primary' 
        ? 'text-blue-600 bg-blue-50 hover:bg-blue-100'
        : 'text-gray-600 bg-gray-50 hover:bg-gray-100'
    }`}
  >
    <Icon className="w-4 h-4 mr-2" />
    {label}
  </button>
);

// Status indicators with icons
const TripStatus = ({ status }) => {
  const statusConfig = {
    planned: { icon: Clock, color: 'text-yellow-600 bg-yellow-100', label: 'Planned' },
    confirmed: { icon: CheckCircle2, color: 'text-green-600 bg-green-100', label: 'Confirmed' },
    completed: { icon: Check, color: 'text-gray-600 bg-gray-100', label: 'Completed' }
  };

  const config = statusConfig[status] || statusConfig.planned;
  const Icon = config.icon;

  return (
    <span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${config.color}`}>
      <Icon className="w-3 h-3 mr-1" />
      {config.label}
    </span>
  );
};

Domain-Specific Icon Usage

// RV and camping context icons
const RvProfile = ({ rv }) => (
  <div className="flex items-center space-x-4">
    <Truck className="w-5 h-5 text-blue-600" />
    <div>
      <p className="font-medium">{rv.manufacturer} {rv.model}</p>
      <p className="text-sm text-gray-500">{rv.length}ft • {rv.electric}</p>
    </div>
  </div>
);

// Activity type indicators
const ActivityIcon = ({ type }) => {
  const icons = {
    gear: CheckSquare,
    meal: Utensils,
    reservation: Calendar,
    member: Users,
    campground: Tent
  };

  const Icon = icons[type] || Activity;
  return <Icon className="w-4 h-4" />;
};

Authentication Flow Patterns

Authentication State Management

const AuthProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Check for existing authentication
    const checkAuth = async () => {
      try {
        // Firebase auth check
        const token = localStorage.getItem('firebase-token');
        if (token) {
          // Validate token and set user
          setIsAuthenticated(true);
          setUser(/* decoded user data */);
        }
      } catch (error) {
        console.error('Auth check failed:', error);
      } finally {
        setIsLoading(false);
      }
    };

    checkAuth();
  }, []);

  const login = async (credentials) => {
    setIsLoading(true);
    try {
      // Firebase authentication
      const result = await signInWithEmailAndPassword(auth, credentials.email, credentials.password);
      setUser(result.user);
      setIsAuthenticated(true);
    } catch (error) {
      throw new Error('Login failed: ' + error.message);
    } finally {
      setIsLoading(false);
    }
  };

  const logout = async () => {
    try {
      await signOut(auth);
      setUser(null);
      setIsAuthenticated(false);
      localStorage.removeItem('firebase-token');
    } catch (error) {
      console.error('Logout failed:', error);
    }
  };

  if (isLoading) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="text-center">
          <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
          <p className="mt-4 text-gray-600">Loading...</p>
        </div>
      </div>
    );
  }

  return (
    <AuthContext.Provider value={{ user, isAuthenticated, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

Protected Route Patterns

const ProtectedRoute = ({ children }) => {
  const { isAuthenticated } = useAuth();

  if (!isAuthenticated) {
    return <LoginComponent />;
  }

  return children;
};

// Usage in main app
const App = () => {
  const { isAuthenticated } = useAuth();

  if (!isAuthenticated) {
    return <OnboardingFlow />;
  }

  return (
    <div className="min-h-screen bg-gray-50">
      <Navigation />
      <MainContent />
    </div>
  );
};
const Modal = ({ isOpen, onClose, title, children, size = 'md' }) => {
  if (!isOpen) return null;

  const sizeClasses = {
    sm: 'max-w-md',
    md: 'max-w-lg', 
    lg: 'max-w-2xl',
    xl: 'max-w-4xl'
  };

  return (
    <div className="fixed inset-0 z-50 overflow-y-auto">
      <div className="flex items-center justify-center min-h-screen px-4">
        <div className="fixed inset-0 bg-black bg-opacity-25" onClick={onClose}></div>

        <div className={`relative bg-white rounded-lg shadow-xl ${sizeClasses[size]} w-full`}>
          <div className="flex items-center justify-between p-6 border-b border-gray-200">
            <h3 className="text-lg font-semibold text-gray-900">{title}</h3>
            <button 
              onClick={onClose}
              className="text-gray-400 hover:text-gray-600"
            >
              <X className="w-5 h-5" />
            </button>
          </div>

          <div className="p-6">
            {children}
          </div>
        </div>
      </div>
    </div>
  );
};

// Usage for invite modal
const InviteMemberModal = ({ isOpen, onClose, planId }) => {
  const [email, setEmail] = useState('');
  const [permission, setPermission] = useState('Viewer');

  const handleInvite = async () => {
    try {
      // Send invitation
      await inviteMember(planId, { email, permission });
      onClose();
      setEmail('');
      setPermission('Viewer');
    } catch (error) {
      console.error('Invite failed:', error);
    }
  };

  return (
    <Modal isOpen={isOpen} onClose={onClose} title="Invite Team Member" size="md">
      <form onSubmit={(e) => { e.preventDefault(); handleInvite(); }} className="space-y-4">
        <div>
          <label className="block text-sm font-medium text-gray-700">Email Address</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
            placeholder="colleague@example.com"
            required
          />
        </div>

        <div>
          <label className="block text-sm font-medium text-gray-700">Permission Level</label>
          <select
            value={permission}
            onChange={(e) => setPermission(e.target.value)}
            className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
          >
            <option value="Viewer">Viewer</option>
            <option value="Editor">Editor</option>
            <option value="Admin">Admin</option>
          </select>
        </div>

        <div className="flex justify-end space-x-3 pt-4">
          <button
            type="button"
            onClick={onClose}
            className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
          >
            Cancel
          </button>
          <button
            type="submit"
            className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
          >
            <Mail className="w-4 h-4 mr-2 inline" />
            Send Invitation
          </button>
        </div>
      </form>
    </Modal>
  );
};

Camping Domain UI Patterns

Trip Planning Components

const TripCard = ({ trip, onEdit, onViewDetails }) => (
  <div className="bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
    <div className="p-6">
      <div className="flex items-start justify-between mb-4">
        <div className="flex items-center space-x-3">
          <Tent className="w-5 h-5 text-green-600" />
          <div>
            <h3 className="font-semibold text-gray-900">{trip.campground}</h3>
            <p className="text-sm text-gray-500">{trip.location}</p>
          </div>
        </div>
        <TripStatus status={trip.status} />
      </div>

      <div className="space-y-2 mb-4">
        <div className="flex items-center text-sm text-gray-600">
          <Calendar className="w-4 h-4 mr-2" />
          {formatDateRange(trip.startDate, trip.endDate)}
        </div>
        <div className="flex items-center text-sm text-gray-600">
          <Users className="w-4 h-4 mr-2" />
          {trip.reservations.length} reservation{trip.reservations.length !== 1 ? 's' : ''}
        </div>
        {trip.rvCompatible && (
          <div className="flex items-center text-sm text-green-600">
            <Truck className="w-4 h-4 mr-2" />
            RV Compatible
          </div>
        )}
      </div>

      <div className="flex justify-between items-center pt-4 border-t border-gray-100">
        <button
          onClick={() => onViewDetails(trip.id)}
          className="text-sm text-blue-600 hover:text-blue-700 font-medium"
        >
          View Details
        </button>
        <button
          onClick={() => onEdit(trip.id)}
          className="inline-flex items-center px-3 py-1 text-xs font-medium text-gray-700 bg-gray-100 rounded-full hover:bg-gray-200"
        >
          <Settings className="w-3 h-3 mr-1" />
          Edit
        </button>
      </div>
    </div>
  </div>
);

RV Compatibility Component

const RvCompatibilityCheck = ({ userRv, campgroundSpecs }) => {
  const checkCompatibility = (spec, required, available) => {
    if (!required) return { status: 'ok', message: 'Not specified' };
    if (!available) return { status: 'unknown', message: 'Unknown' };

    const isCompatible = available >= required;
    return {
      status: isCompatible ? 'ok' : 'warning',
      message: isCompatible ? 'Compatible' : `Requires ${required}, available ${available}`
    };
  };

  const checks = [
    {
      label: 'Length',
      check: checkCompatibility('length', userRv?.length, campgroundSpecs?.maxLength)
    },
    {
      label: 'Width', 
      check: checkCompatibility('width', userRv?.width, campgroundSpecs?.maxWidth)
    },
    {
      label: 'Height',
      check: checkCompatibility('height', userRv?.height, campgroundSpecs?.maxHeight)
    }
  ];

  return (
    <div className="bg-gray-50 rounded-lg p-4">
      <div className="flex items-center mb-3">
        <Truck className="w-4 h-4 text-blue-600 mr-2" />
        <span className="font-medium text-gray-900">RV Compatibility</span>
      </div>

      <div className="space-y-2">
        {checks.map((item, index) => (
          <div key={index} className="flex items-center justify-between">
            <span className="text-sm text-gray-600">{item.label}</span>
            <div className="flex items-center">
              {item.check.status === 'ok' && <CheckCircle2 className="w-4 h-4 text-green-500 mr-1" />}
              {item.check.status === 'warning' && <AlertTriangle className="w-4 h-4 text-yellow-500 mr-1" />}
              {item.check.status === 'unknown' && <Info className="w-4 h-4 text-gray-400 mr-1" />}
              <span className="text-sm text-gray-700">{item.check.message}</span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

Gear and Meal Planning Components

const GearItem = ({ item, onClaim, onUnclaim, currentUserId }) => {
  const isClaimed = item.claimedBy;
  const isClaimedByUser = item.claimedBy?.id === currentUserId;

  return (
    <div className={`flex items-center justify-between p-3 rounded-md border ${
      isClaimed 
        ? isClaimedByUser 
          ? 'bg-green-50 border-green-200' 
          : 'bg-gray-50 border-gray-200'
        : 'bg-white border-gray-300'
    }`}>
      <div className="flex items-center space-x-3">
        <CheckSquare className={`w-4 h-4 ${isClaimed ? 'text-green-600' : 'text-gray-400'}`} />
        <div>
          <p className="font-medium text-gray-900">{item.name}</p>
          {item.notes && (
            <p className="text-sm text-gray-500">{item.notes}</p>
          )}
        </div>
      </div>

      <div className="flex items-center space-x-2">
        {isClaimed ? (
          <>
            <span className="text-sm text-gray-600">
              {isClaimedByUser ? 'You' : item.claimedBy.name}
            </span>
            {isClaimedByUser && (
              <button
                onClick={() => onUnclaim(item.id)}
                className="text-xs text-red-600 hover:text-red-700"
              >
                Unclaim
              </button>
            )}
          </>
        ) : (
          <button
            onClick={() => onClaim(item.id)}
            className="text-xs text-blue-600 hover:text-blue-700 font-medium"
          >
            Claim
          </button>
        )}
      </div>
    </div>
  );
};

Loading and Error State Patterns

Loading States

const LoadingSpinner = ({ size = 'md', message = 'Loading...' }) => {
  const sizeClasses = {
    sm: 'h-4 w-4',
    md: 'h-8 w-8', 
    lg: 'h-12 w-12'
  };

  return (
    <div className="flex flex-col items-center justify-center py-8">
      <div className={`animate-spin rounded-full border-b-2 border-blue-600 ${sizeClasses[size]}`}></div>
      <p className="mt-4 text-gray-600 text-sm">{message}</p>
    </div>
  );
};

// Skeleton loading for cards
const TripCardSkeleton = () => (
  <div className="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
    <div className="animate-pulse">
      <div className="flex items-center space-x-3 mb-4">
        <div className="w-5 h-5 bg-gray-300 rounded"></div>
        <div className="flex-1">
          <div className="h-4 bg-gray-300 rounded w-3/4 mb-2"></div>
          <div className="h-3 bg-gray-300 rounded w-1/2"></div>
        </div>
      </div>
      <div className="space-y-2 mb-4">
        <div className="h-3 bg-gray-300 rounded"></div>
        <div className="h-3 bg-gray-300 rounded w-2/3"></div>
      </div>
    </div>
  </div>
);

Error State Components

const ErrorBoundary = ({ error, onRetry, children }) => {
  if (error) {
    return (
      <div className="rounded-md bg-red-50 p-4">
        <div className="flex">
          <div className="flex-shrink-0">
            <AlertTriangle className="h-5 w-5 text-red-400" />
          </div>
          <div className="ml-3">
            <h3 className="text-sm font-medium text-red-800">Something went wrong</h3>
            <div className="mt-2 text-sm text-red-700">
              <p>{error.message || 'An unexpected error occurred'}</p>
            </div>
            {onRetry && (
              <div className="mt-4">
                <button
                  onClick={onRetry}
                  className="bg-red-100 px-2 py-1 text-xs font-semibold text-red-800 rounded"
                >
                  Try again
                </button>
              </div>
            )}
          </div>
        </div>
      </div>
    );
  }

  return children;
};

Testing Patterns

Component Testing Setup

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi } from 'vitest';
import TripCard from './TripCard';

const mockTrip = {
  id: 't1',
  campground: 'Yosemite Valley',
  location: 'California',
  startDate: '2024-06-15',
  endDate: '2024-06-20',
  status: 'confirmed',
  reservations: [{ userId: 'u1', siteNumber: 'A23' }],
  rvCompatible: true
};

describe('TripCard', () => {
  const mockOnEdit = vi.fn();
  const mockOnViewDetails = vi.fn();

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

  it('renders trip information correctly', () => {
    render(
      <TripCard 
        trip={mockTrip} 
        onEdit={mockOnEdit} 
        onViewDetails={mockOnViewDetails}
      />
    );

    expect(screen.getByText('Yosemite Valley')).toBeInTheDocument();
    expect(screen.getByText('California')).toBeInTheDocument();
    expect(screen.getByText('RV Compatible')).toBeInTheDocument();
    expect(screen.getByText('1 reservation')).toBeInTheDocument();
  });

  it('calls onEdit when edit button is clicked', () => {
    render(
      <TripCard 
        trip={mockTrip} 
        onEdit={mockOnEdit} 
        onViewDetails={mockOnViewDetails}
      />
    );

    fireEvent.click(screen.getByText('Edit'));
    expect(mockOnEdit).toHaveBeenCalledWith('t1');
  });

  it('calls onViewDetails when view details button is clicked', () => {
    render(
      <TripCard 
        trip={mockTrip} 
        onEdit={mockOnEdit} 
        onViewDetails={mockOnViewDetails}
      />
    );

    fireEvent.click(screen.getByText('View Details'));
    expect(mockOnViewDetails).toHaveBeenCalledWith('t1');
  });
});

Performance Optimization

React.memo for Expensive Components

const TripCard = React.memo(({ trip, onEdit, onViewDetails }) => {
  // Component implementation
}, (prevProps, nextProps) => {
  // Custom comparison for optimization
  return (
    prevProps.trip.id === nextProps.trip.id &&
    prevProps.trip.status === nextProps.trip.status &&
    prevProps.trip.reservations.length === nextProps.trip.reservations.length
  );
});

useCallback for Event Handlers

const PlanManager = () => {
  const [plans, setPlans] = useState([]);

  const handleEditPlan = useCallback((planId) => {
    // Edit logic
  }, []);

  const handleDeletePlan = useCallback((planId) => {
    setPlans(prev => prev.filter(p => p.id !== planId));
  }, []);

  return (
    <div>
      {plans.map(plan => (
        <PlanCard 
          key={plan.id}
          plan={plan}
          onEdit={handleEditPlan}
          onDelete={handleDeletePlan}
        />
      ))}
    </div>
  );
};

Remember: This skill automatically activates when working on React component tasks. Always prioritize user experience, accessibility, and the collaborative camping domain context in your component implementations.