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 (