From 06f5bc2c86f402d57fa06abda7afaeb83407bdd8 Mon Sep 17 00:00:00 2001 From: Maurice <61554723+mauriceboe@users.noreply.github.com> Date: Thu, 6 Aug 2026 21:59:30 +0200 Subject: [PATCH] fix(planner): keep the file-upload callback above the place bail-out (#1800) PlaceInspector returns null when no place is selected, but the useCallback for handleFileUpload sat below that return. Clearing the selection therefore changed the hook count and React threw "Rendered fewer hooks than expected", which takes the whole tree down since nothing catches it. Move the callback above the bail-out and read the id through place?.id. --- .../Planner/PlaceInspector.test.tsx | 14 ++++++ .../src/components/Planner/PlaceInspector.tsx | 46 ++++++++++--------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/client/src/components/Planner/PlaceInspector.test.tsx b/client/src/components/Planner/PlaceInspector.test.tsx index e1577d0da..8790ca3e8 100644 --- a/client/src/components/Planner/PlaceInspector.test.tsx +++ b/client/src/components/Planner/PlaceInspector.test.tsx @@ -1280,4 +1280,18 @@ describe('PlaceInspector', () => { await waitFor(() => expect(onUploadImage).toHaveBeenCalledWith(place.id, file)); }); + it('FE-PLANNER-INSPECTOR-098: deselecting and reselecting a place survives a rerender', async () => { + // The component bails out with `if (!place) return null`. Any hook below that + // line runs only while a place is selected, so clearing the selection changes + // the hook count and React tears the whole tree down. + const { rerender } = render(); + expect(screen.getByText('Eiffel Tower')).toBeTruthy(); + + rerender(); + expect(screen.queryByText('Eiffel Tower')).toBeNull(); + + rerender(); + expect(screen.getByText('Eiffel Tower')).toBeTruthy(); + }); + }); diff --git a/client/src/components/Planner/PlaceInspector.tsx b/client/src/components/Planner/PlaceInspector.tsx index 5c20c14f2..5efe20a39 100644 --- a/client/src/components/Planner/PlaceInspector.tsx +++ b/client/src/components/Planner/PlaceInspector.tsx @@ -231,6 +231,31 @@ export default function PlaceInspector({ }) }, [place, openSavePicker]) + // Sits above the `if (!place)` bail-out below: a hook after an early return is + // only reached while a place is selected, so deselecting one mid-session + // changes the hook count and React tears the tree down. + const placeId = place?.id + const handleFileUpload = useCallback(async (e) => { + const selectedFiles = Array.from((e.target as HTMLInputElement).files || []) + if (!selectedFiles.length || !onFileUpload || !placeId) return + setIsUploading(true) + try { + for (const file of selectedFiles) { + const fd = new FormData() + fd.append('file', file) + fd.append('place_id', String(placeId)) + await onFileUpload(fd) + } + setFilesExpanded(true) + } catch (err: unknown) { + console.error('Upload failed', err) + toast.error(translateApiError(t, err, 'files.uploadError')) + } finally { + setIsUploading(false) + if (fileInputRef.current) fileInputRef.current.value = '' + } + }, [onFileUpload, placeId, toast, t]) + const startNameEdit = () => { if (!onUpdatePlace) return setNameValue(place.name || '') @@ -283,27 +308,6 @@ export default function PlaceInspector({ const placeFiles = (files || []).filter(f => String(f.place_id) === String(place.id) || (f.linked_place_ids || []).includes(place.id)) - const handleFileUpload = useCallback(async (e) => { - const selectedFiles = Array.from((e.target as HTMLInputElement).files || []) - if (!selectedFiles.length || !onFileUpload) return - setIsUploading(true) - try { - for (const file of selectedFiles) { - const fd = new FormData() - fd.append('file', file) - fd.append('place_id', String(place.id)) - await onFileUpload(fd) - } - setFilesExpanded(true) - } catch (err: unknown) { - console.error('Upload failed', err) - toast.error(translateApiError(t, err, 'files.uploadError')) - } finally { - setIsUploading(false) - if (fileInputRef.current) fileInputRef.current.value = '' - } - }, [onFileUpload, place.id, toast, t]) - return (