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.
This commit is contained in:
Maurice
2026-08-06 21:59:30 +02:00
committed by GitHub
parent ab9ca4220a
commit 06f5bc2c86
2 changed files with 39 additions and 21 deletions
@@ -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(<PlaceInspector {...defaultProps} />);
expect(screen.getByText('Eiffel Tower')).toBeTruthy();
rerender(<PlaceInspector {...defaultProps} place={null} />);
expect(screen.queryByText('Eiffel Tower')).toBeNull();
rerender(<PlaceInspector {...defaultProps} />);
expect(screen.getByText('Eiffel Tower')).toBeTruthy();
});
});
@@ -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 (
<div
style={{