Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2025-05-14 - [React Context & Provider Shadowing]
**Learning:** Redundant context providers can cause "shadowing" where children update an inner provider's state while the parent consumes an outer provider's stale state. This often leads to bugs and unnecessary re-renders. Additionally, unmemoized context values cause all consumers to re-render whenever the provider re-renders, even if the consumed state hasn't changed.

**Action:** Always memoize context value objects with `useMemo` and functions with `useCallback`. Audit the component tree to remove redundant providers, ensuring a single source of truth for each context while maintaining proper state isolation (e.g., keeping chat-specific data at the page/chat level rather than the root layout if session isolation is required).
13 changes: 9 additions & 4 deletions components/calendar-toggle-context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { createContext, useContext, useState, ReactNode, useTransition } from 'react'
import { createContext, useContext, useState, ReactNode, useTransition, useMemo, useCallback } from 'react'

interface CalendarToggleContextType {
isCalendarOpen: boolean
Expand All @@ -21,14 +21,19 @@ export const CalendarToggleProvider = ({ children }: { children: ReactNode }) =>
const [isPending, startTransition] = useTransition()
const [isCalendarOpen, setIsCalendarOpen] = useState(false)

const toggleCalendar = () => {
const toggleCalendar = useCallback(() => {
startTransition(() => {
setIsCalendarOpen(prevState => !prevState)
})
}
}, [])

const value = useMemo(() => ({
isCalendarOpen,
toggleCalendar
}), [isCalendarOpen, toggleCalendar])

return (
<CalendarToggleContext.Provider value={{ isCalendarOpen, toggleCalendar }}>
<CalendarToggleContext.Provider value={value}>
{children}
</CalendarToggleContext.Provider>
)
Expand Down
16 changes: 6 additions & 10 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ 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 { useMapData } from './map/map-data-context'; // Add this and useMapData
import { updateDrawingContext } from '@/lib/actions/chat'; // Import the server action
import dynamic from 'next/dynamic'
import { HeaderSearchButton } from './header-search-button'
Expand All @@ -36,7 +36,7 @@ export function Chat({ id }: ChatProps) {
const { isUsageOpen } = useUsageToggle();
const { isCalendarOpen } = useCalendarToggle()
const [input, setInput] = useState('')
const [showEmptyScreen, setShowEmptyScreen] = useState(false)
const showEmptyScreen = messages.length === 0
const [isSubmitting, setIsSubmitting] = useState(false)
const [suggestions, setSuggestions] = useState<PartialRelated | null>(null)
const chatPanelRef = useRef<ChatPanelRef>(null);
Expand All @@ -49,10 +49,6 @@ export function Chat({ id }: ChatProps) {
chatPanelRef.current?.submitForm();
};

useEffect(() => {
setShowEmptyScreen(messages.length === 0)
}, [messages])

useEffect(() => {
// Check if device is mobile
const checkMobile = () => {
Expand Down Expand Up @@ -125,7 +121,7 @@ export function Chat({ id }: ChatProps) {
// Mobile layout
if (isMobile) {
return (
<MapDataProvider> {/* Add Provider */}
<>
<HeaderSearchButton />
<div className="mobile-layout-container">
<div className="mobile-map-section">
Expand Down Expand Up @@ -165,13 +161,13 @@ export function Chat({ id }: ChatProps) {
)}
</div>
</div>
</MapDataProvider>
</>
);
}

// Desktop layout
return (
<MapDataProvider> {/* Add Provider */}
<>
<HeaderSearchButton />
<div className="flex justify-start items-start">
{/* This is the new div for scrolling */}
Expand Down Expand Up @@ -211,6 +207,6 @@ export function Chat({ id }: ChatProps) {
{activeView ? <SettingsView /> : isUsageOpen ? <UsageView /> : <MapProvider />}
</div>
</div>
</MapDataProvider>
</>
);
}
14 changes: 10 additions & 4 deletions components/history-toggle-context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { createContext, useContext, useState, ReactNode } from "react"
import { createContext, useContext, useState, ReactNode, useMemo, useCallback } from "react"

interface HistoryToggleContextType {
isHistoryOpen: boolean
Expand All @@ -13,11 +13,17 @@ const HistoryToggleContext = createContext<HistoryToggleContextType | undefined>
export const HistoryToggleProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [isHistoryOpen, setIsHistoryOpen] = useState(false)

const toggleHistory = () => setIsHistoryOpen(prev => !prev)
const setHistoryOpen = (open: boolean) => setIsHistoryOpen(open)
const toggleHistory = useCallback(() => setIsHistoryOpen(prev => !prev), [])
const setHistoryOpen = useCallback((open: boolean) => setIsHistoryOpen(open), [])

const value = useMemo(() => ({
isHistoryOpen,
toggleHistory,
setHistoryOpen
}), [isHistoryOpen, toggleHistory, setHistoryOpen])

return (
<HistoryToggleContext.Provider value={{ isHistoryOpen, toggleHistory, setHistoryOpen }}>
<HistoryToggleContext.Provider value={value}>
{children}
</HistoryToggleContext.Provider>
)
Expand Down
14 changes: 12 additions & 2 deletions components/map-loading-context.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use client';
import { createContext, useContext, useState, ReactNode } from 'react';
import { createContext, useContext, useState, ReactNode, useMemo, useCallback } from 'react';

interface MapLoadingContextType {
isMapLoaded: boolean;
Expand All @@ -10,8 +10,18 @@ const MapLoadingContext = createContext<MapLoadingContextType | undefined>(undef

export const MapLoadingProvider = ({ children }: { children: ReactNode }) => {
const [isMapLoaded, setIsMapLoaded] = useState(false);

const setIsMapLoadedCallback = useCallback((isLoaded: boolean) => {
setIsMapLoaded(isLoaded);
}, []);

const value = useMemo(() => ({
isMapLoaded,
setIsMapLoaded: setIsMapLoadedCallback
}), [isMapLoaded, setIsMapLoadedCallback]);

return (
<MapLoadingContext.Provider value={{ isMapLoaded, setIsMapLoaded }}>
<MapLoadingContext.Provider value={value}>
{children}
</MapLoadingContext.Provider>
);
Expand Down
13 changes: 9 additions & 4 deletions components/map-toggle-context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import React, { createContext, useContext, useState, ReactNode } from 'react';
import React, { createContext, useContext, useState, ReactNode, useMemo, useCallback } from 'react';

export enum MapToggleEnum {
FreeMode,
Expand All @@ -22,12 +22,17 @@ interface MapToggleProviderProps {
export const MapToggleProvider: React.FC<MapToggleProviderProps> = ({ children }) => {
const [mapToggleState, setMapToggle] = useState<MapToggleEnum>(MapToggleEnum.FreeMode);

const setMapType = (type: MapToggleEnum) => {
const setMapType = useCallback((type: MapToggleEnum) => {
setMapToggle(type);
}
}, [])

const value = useMemo(() => ({
mapType: mapToggleState,
setMapType
}), [mapToggleState, setMapType])

return (
<MapToggleContext.Provider value={{ mapType: mapToggleState, setMapType }}>
<MapToggleContext.Provider value={value}>
{children}
</MapToggleContext.Provider>
);
Expand Down
9 changes: 7 additions & 2 deletions components/map/map-data-context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import React, { createContext, useContext, useState, ReactNode } from 'react';
import React, { createContext, useContext, useState, ReactNode, useMemo } from 'react';
// Define the shape of the map data you want to share
export interface CameraState {
center: { lat: number; lng: number };
Expand Down Expand Up @@ -41,8 +41,13 @@ const MapDataContext = createContext<MapDataContextType | undefined>(undefined);
export const MapDataProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [mapData, setMapData] = useState<MapData>({ drawnFeatures: [], markers: [] });

const value = useMemo(() => ({
mapData,
setMapData
}), [mapData])

return (
<MapDataContext.Provider value={{ mapData, setMapData }}>
<MapDataContext.Provider value={value}>
{children}
</MapDataContext.Provider>
);
Expand Down
18 changes: 12 additions & 6 deletions components/profile-toggle-context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// components/
'use client'
import { createContext, useContext, useState, ReactNode } from "react"
import { createContext, useContext, useState, ReactNode, useMemo, useCallback } from "react"
//import profile-toggle-context.tsx;

export enum ProfileToggleEnum {
Expand All @@ -25,16 +25,22 @@ interface ProfileToggleProviderProps {
export const ProfileToggleProvider: React.FC<ProfileToggleProviderProps> = ({ children }) => {
const [activeView, setActiveView] = useState<ProfileToggleEnum | null>(null)

const toggleProfileSection = (section: ProfileToggleEnum) => {
const toggleProfileSection = useCallback((section: ProfileToggleEnum) => {
setActiveView(prevView => (prevView === section ? null : section))
}
}, [])

const closeProfileView = () => {
const closeProfileView = useCallback(() => {
setActiveView(null)
}
}, [])

const value = useMemo(() => ({
activeView,
toggleProfileSection,
closeProfileView
}), [activeView, toggleProfileSection, closeProfileView])

return (
<ProfileToggleContext.Provider value={{ activeView, toggleProfileSection, closeProfileView }}>
<ProfileToggleContext.Provider value={value}>
{children}
</ProfileToggleContext.Provider>
)
Expand Down
14 changes: 10 additions & 4 deletions components/usage-toggle-context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { createContext, useContext, useState, ReactNode } from "react"
import { createContext, useContext, useState, ReactNode, useMemo, useCallback } from "react"

interface UsageToggleContextType {
isUsageOpen: boolean
Expand All @@ -13,11 +13,17 @@ const UsageToggleContext = createContext<UsageToggleContextType | undefined>(und
export const UsageToggleProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [isUsageOpen, setIsUsageOpen] = useState(false)

const toggleUsage = () => setIsUsageOpen(prev => !prev)
const closeUsage = () => setIsUsageOpen(false)
const toggleUsage = useCallback(() => setIsUsageOpen(prev => !prev), [])
const closeUsage = useCallback(() => setIsUsageOpen(false), [])

const value = useMemo(() => ({
isUsageOpen,
toggleUsage,
closeUsage
}), [isUsageOpen, toggleUsage, closeUsage])

return (
<UsageToggleContext.Provider value={{ isUsageOpen, toggleUsage, closeUsage }}>
<UsageToggleContext.Provider value={value}>
{children}
</UsageToggleContext.Provider>
)
Expand Down