fix(store): reset and uniformly hydrate trip-scoped slices in loadTrip (H4, H5) (#1180)

loadTrip only replaced the first slice group, so budget/reservations/files
from a previous trip stayed visible after switching trips (data exposure on a
shared screen). Those three also loaded via separate tab-gated effects, so they
never hydrated offline for an unopened tab.

- resetTrip() clears every trip-scoped slice (keeps global tags/categories) and
  runs at the top of loadTrip, so a switch can't leak the prior trip's data
- loadTrip now hydrates budget/reservations/files through their repos alongside
  the rest (non-fatal catches), making offline hydration uniform
- useTripPlanner drops the redundant loadFiles + reservations/budget effects;
  tab-gated lazy reloads stay as on-demand refresh
- tests: cross-trip no-leak, uniform hydration, resetTrip
This commit is contained in:
jubnl
2026-06-15 09:25:28 +02:00
committed by GitHub
parent bcd2c8c959
commit 1eb2cb8eb2
3 changed files with 116 additions and 11 deletions
@@ -221,11 +221,12 @@ export function useTripPlanner() {
}
}, [isLoading, places])
// Load trip + files (needed for place inspector file section)
// Load the trip. loadTrip hydrates every trip-scoped slice (days, places,
// packing, todo, budget, reservations, files) so offline hydration is uniform
// and there's no cross-trip bleed; members/accommodations load alongside.
useEffect(() => {
if (tripId) {
tripActions.loadTrip(tripId).catch(() => { toast.error(t('trip.toast.loadError')); navigate('/dashboard') })
tripActions.loadFiles(tripId)
loadAccommodations()
if (!navigator.onLine) {
offlineDb.tripMembers.where('tripId').equals(Number(tripId)).toArray()
@@ -240,13 +241,6 @@ export function useTripPlanner() {
}
}, [tripId])
useEffect(() => {
if (tripId) {
tripActions.loadReservations(tripId)
tripActions.loadBudgetItems?.(tripId)
}
}, [tripId])
useTripWebSocket(tripId)
const [mapCategoryFilter, setMapCategoryFilter] = useState<Set<string>>(new Set())
+33 -1
View File
@@ -7,6 +7,9 @@ import { dayRepo } from '../repo/dayRepo'
import { placeRepo } from '../repo/placeRepo'
import { packingRepo } from '../repo/packingRepo'
import { todoRepo } from '../repo/todoRepo'
import { budgetRepo } from '../repo/budgetRepo'
import { reservationRepo } from '../repo/reservationRepo'
import { fileRepo } from '../repo/fileRepo'
import { createPlacesSlice } from './slices/placesSlice'
import { createAssignmentsSlice } from './slices/assignmentsSlice'
import { createDaysSlice } from './slices/daysSlice'
@@ -61,6 +64,7 @@ export interface TripStoreState
setSelectedDay: (dayId: number | null) => void
handleRemoteEvent: (event: WebSocketEvent) => void
resetTrip: () => void
loadTrip: (tripId: number | string) => Promise<void>
refreshDays: (tripId: number | string) => Promise<void>
updateTrip: (tripId: number | string, data: Partial<Trip>) => Promise<Trip>
@@ -89,15 +93,40 @@ export const useTripStore = create<TripStoreState>((set, get) => ({
handleRemoteEvent: (event: WebSocketEvent) => handleRemoteEvent(set, get, event),
// Clear every trip-scoped slice so switching trips (or losing access to one)
// can never leave a previous trip's data visible. Global tags/categories are
// left intact. Called at the top of loadTrip.
resetTrip: () => set({
trip: null,
days: [],
places: [],
assignments: {},
dayNotes: {},
packingItems: [],
todoItems: [],
budgetItems: [],
files: [],
reservations: [],
selectedDayId: null,
error: null,
}),
loadTrip: async (tripId: number | string) => {
get().resetTrip()
set({ isLoading: true, error: null })
try {
const [tripData, daysData, placesData, packingData, todoData, tagsData, categoriesData] = await Promise.all([
const [tripData, daysData, placesData, packingData, todoData, budgetData, reservationsData, filesData, tagsData, categoriesData] = await Promise.all([
tripRepo.get(tripId),
dayRepo.list(tripId),
placeRepo.list(tripId),
packingRepo.list(tripId),
todoRepo.list(tripId),
// Budget / reservations / files are hydrated here too so the offline
// path is uniform (no separate tab-gated effects). Non-fatal: a failure
// in any of these must not blank the whole trip.
budgetRepo.list(tripId).catch(() => ({ items: [] as BudgetItem[] })),
reservationRepo.list(tripId).catch(() => ({ reservations: [] as Reservation[] })),
fileRepo.list(tripId).catch(() => ({ files: [] as TripFile[] })),
navigator.onLine
? tagsApi.list().catch(() => offlineDb.tags.toArray().then(tags => ({ tags })))
: offlineDb.tags.toArray().then(tags => ({ tags })),
@@ -121,6 +150,9 @@ export const useTripStore = create<TripStoreState>((set, get) => ({
dayNotes: dayNotesMap,
packingItems: packingData.items,
todoItems: todoData.items,
budgetItems: budgetData.items,
reservations: reservationsData.reservations,
files: filesData.files,
tags: tagsData.tags,
categories: categoriesData.categories,
isLoading: false,