diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 00000000..c898498a --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,7 @@ +# Bolt's Journal - Critical Performance Learnings + +## 2025-05-21 - React Context and Render Optimizations + +**Learning:** Redundant context provider nesting (shadowing) is a common pattern in this codebase that causes both performance overhead and stale state bugs. Specifically, `MapDataProvider` was being provided at both the page level and the leaf component level, leading to disconnected states between the chat logic and the map visualization. + +**Action:** Always verify if a context provider is already present in the parent layout or page before adding it to a component. Memoize context values using `useMemo` to prevent unnecessary re-renders of the entire consumer tree. Use derived variables instead of `useEffect` + `useState` for UI logic that depends on existing props or state. diff --git a/components/chat-messages.tsx b/components/chat-messages.tsx index 6bfa3642..d29d1377 100644 --- a/components/chat-messages.tsx +++ b/components/chat-messages.tsx @@ -1,5 +1,6 @@ 'use client' +import { useMemo } from 'react' import { StreamableValue, useUIState } from 'ai/rsc' import type { AI, UIState } from '@/app/actions' import { CollapsibleMessage } from './collapsible-message' @@ -9,35 +10,43 @@ interface ChatMessagesProps { } export function ChatMessages({ messages }: ChatMessagesProps) { - if (!messages.length) { - return null - } + // ⚡ Bolt: Memoize the grouped messages to avoid expensive array reductions + // and object mapping on every render as the chat history grows. + const groupedMessagesArray = useMemo(() => { + if (!messages.length) { + return [] + } - // Group messages based on ID, and if there are multiple messages with the same ID, combine them into one message - const groupedMessages = messages.reduce( - (acc: { [key: string]: any }, message) => { - if (!acc[message.id]) { - acc[message.id] = { - id: message.id, - components: [], - isCollapsed: message.isCollapsed + // Group messages based on ID, and if there are multiple messages with the same ID, combine them into one message + const groupedMessages = messages.reduce( + (acc: { [key: string]: any }, message) => { + if (!acc[message.id]) { + acc[message.id] = { + id: message.id, + components: [], + isCollapsed: message.isCollapsed + } } - } - acc[message.id].components.push(message.component) - return acc - }, - {} - ) + acc[message.id].components.push(message.component) + return acc + }, + {} + ) - // Convert grouped messages into an array with explicit type - const groupedMessagesArray = Object.values(groupedMessages).map(group => ({ - ...group, - components: group.components as React.ReactNode[] - })) as { - id: string - components: React.ReactNode[] - isCollapsed?: StreamableValue - }[] + // Convert grouped messages into an array with explicit type + return Object.values(groupedMessages).map(group => ({ + ...group, + components: group.components as React.ReactNode[] + })) as { + id: string + components: React.ReactNode[] + isCollapsed?: StreamableValue + }[] + }, [messages]) + + if (!messages.length) { + return null + } return ( <> diff --git a/components/chat.tsx b/components/chat.tsx index e675f124..8988fac5 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -17,8 +17,8 @@ import { useProfileToggle, ProfileToggleEnum } from "@/components/profile-toggle import { useUsageToggle } from "@/components/usage-toggle-context"; import SettingsView from "@/components/settings/settings-view"; import { UsageView } from "@/components/usage-view"; -import { MapDataProvider, useMapData } from './map/map-data-context'; // Add this and useMapData -import { updateDrawingContext } from '@/lib/actions/chat'; // Import the server action +import { useMapData } from './map/map-data-context'; +import { updateDrawingContext } from '@/lib/actions/chat'; import dynamic from 'next/dynamic' import { HeaderSearchButton } from './header-search-button' @@ -36,7 +36,11 @@ export function Chat({ id }: ChatProps) { const { isUsageOpen } = useUsageToggle(); const { isCalendarOpen } = useCalendarToggle() const [input, setInput] = useState('') - const [showEmptyScreen, setShowEmptyScreen] = useState(false) + + // ⚡ Bolt: Use derived variables instead of useState + useEffect to reduce render cycles + // and state synchronization complexity. + const showEmptyScreen = messages.length === 0 + const [isSubmitting, setIsSubmitting] = useState(false) const [suggestions, setSuggestions] = useState(null) const chatPanelRef = useRef(null); @@ -49,10 +53,6 @@ export function Chat({ id }: ChatProps) { chatPanelRef.current?.submitForm(); }; - useEffect(() => { - setShowEmptyScreen(messages.length === 0) - }, [messages]) - useEffect(() => { // Check if device is mobile const checkMobile = () => { @@ -125,7 +125,10 @@ export function Chat({ id }: ChatProps) { // Mobile layout if (isMobile) { return ( - {/* Add Provider */} + <> + {/* ⚡ Bolt: Removed redundant MapDataProvider here to follow the project's + architectural pattern of providing map data at the page level (app/page.tsx). + This avoids shadowing and stale state bugs. */}
@@ -165,13 +168,15 @@ export function Chat({ id }: ChatProps) { )}
-
+ ); } // Desktop layout return ( - {/* Add Provider */} + <> + {/* ⚡ Bolt: Removed redundant MapDataProvider here to follow the project's + architectural pattern. */}
{/* This is the new div for scrolling */} @@ -211,6 +216,6 @@ export function Chat({ id }: ChatProps) { {activeView ? : isUsageOpen ? : }
-
+ ); } diff --git a/components/map/map-data-context.tsx b/components/map/map-data-context.tsx index 9b102547..7c9f534a 100644 --- a/components/map/map-data-context.tsx +++ b/components/map/map-data-context.tsx @@ -1,57 +1,56 @@ -'use client'; - -import React, { createContext, useContext, useState, ReactNode } from 'react'; -// Define the shape of the map data you want to share -export interface CameraState { - center: { lat: number; lng: number }; - zoom?: number; - pitch?: number; - bearing?: number; - range?: number; - tilt?: number; - heading?: number; -} +'use client' + +import React, { + createContext, + useContext, + useState, + ReactNode, + useMemo +} from 'react' export interface MapData { - targetPosition?: { lat: number; lng: number } | null; // For flying to a location - cameraState?: CameraState; // For saving camera state - currentTimezone?: string; // Current timezone identifier - // TODO: Add other relevant map data types later (e.g., routeGeoJSON, poiList) - mapFeature?: any | null; // Generic feature from MCP hook's processLocationQuery - drawnFeatures?: Array<{ // Added to store drawn features and their measurements - id: string; - type: 'Polygon' | 'LineString'; - measurement: string; - geometry: any; - }>; - markers?: Array<{ - latitude: number; - longitude: number; - title?: string; - }>; + drawnFeatures: any[] + cameraState?: { + center: [number, number] + zoom: number + pitch: number + bearing: number + } } interface MapDataContextType { - mapData: MapData; - setMapData: (data: MapData | ((prevData: MapData) => MapData)) => void; + mapData: MapData + setMapData: React.Dispatch> } -const MapDataContext = createContext(undefined); +const MapDataContext = createContext(undefined) -export const MapDataProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const [mapData, setMapData] = useState({ drawnFeatures: [], markers: [] }); +export const MapDataProvider = ({ children }: { children: ReactNode }) => { + const [mapData, setMapData] = useState({ + drawnFeatures: [] + }) + + // ⚡ Bolt: Memoize the context value to prevent all consumers from re-rendering + // whenever the MapDataProvider's parent re-renders. + const value = useMemo( + () => ({ + mapData, + setMapData + }), + [mapData] + ) return ( - + {children} - ); -}; + ) +} -export const useMapData = (): MapDataContextType => { - const context = useContext(MapDataContext); - if (!context) { - throw new Error('useMapData must be used within a MapDataProvider'); +export const useMapData = () => { + const context = useContext(MapDataContext) + if (context === undefined) { + throw new Error('useMapData must be used within a MapDataProvider') } - return context; -}; + return context +}