Skip to content

Fix iOS Upcoming Trips selection not updating#1891

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/fix-upcoming-trips-selection
Draft

Fix iOS Upcoming Trips selection not updating#1891
Copilot wants to merge 3 commits intomainfrom
copilot/fix-upcoming-trips-selection

Conversation

Copy link
Contributor

Copilot AI commented Mar 9, 2026

  • Fix iOS upcoming trips selection issue (extraData, ID-based state, stable keyExtractor)
  • Remove useMemo from upcomingTrips to avoid stale Date.now() snapshot
  • Fix useEffect to reset selection when selected trip is removed or list empties
  • Embed trip object in data items; use info.item.trip in renderItem (no index lookup)
  • Remove unused assertDefined import
  • Use optional chaining on upcomingTrips[0]?.id for safety
Original prompt

This section details on the original issue you should resolve

<issue_title>iOS – Upcoming Trips Screen Fails to Update Selection</issue_title>
<issue_description>Description:
On iOS devices, in the Upcoming Trips screen:

  • The first trip is automatically selected when the screen loads.
  • Attempting to select other trips does not change the selection — the first trip remains highlighted.
  • On Android, selecting other trips works correctly.

Steps to Reproduce:

  1. Open the app on iOS
  2. Navigate to Upcoming Trips
  3. Observe that the first trip is automatically selected
  4. Tap on any other trip
  5. Observe that the selection does not change

Screenshots / Screen Recordings:

ScreenRecording_02-25-2026.19-21-00_1.mov

Expected Result:
Selecting any trip should update the selection and display the corresponding trip details.

Actual Result:
The first trip remains selected; tapping other trips has no effect.

Environment:

  • Device: iPhone (13 Pro)

Additional Notes:

  • iOS-specific issue</issue_description>

Comments on the Issue (you are @copilot in this section)

@andrew-bierman @copilot Assigned for POC development. Please investigate and create a proof of concept implementation.

🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Summary by CodeRabbit

  • Refactor
    • Optimized the upcoming trips screen rendering logic to improve responsiveness when viewing and selecting trips.

- Add extraData={selectedTripId} to List so iOS re-renders items on selection change
- Store selectedTripId (string) instead of full trip object to avoid stale refs
- Memoize upcomingTrips with useMemo to prevent unnecessary effect runs
- Use trip id as keyExtractor for stable React reconciliation
- Include id in data items for the list

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix selection issue on iOS for Upcoming Trips screen Fix iOS Upcoming Trips selection not updating Mar 9, 2026
@andrew-bierman
Copy link
Collaborator

@coderabbitai Please review this PR for code quality, best practices, and potential issues.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the list of changed files in the PR
gh pr diff 1891 --repo PackRat-AI/PackRat

Length of output: 3125


@andrew-bierman: Happy to review! Overall the PR is a solid, well-reasoned fix — the core changes (extraData, ID-based state, useMemo, stable keyExtractor) are correct and follow React Native best practices. Here are a few specific observations:


✅ What's done well

  • extraData={selectedTripId} — correct use of the pattern; signals FlatList/SectionList to re-render items on selection change.
  • ID-based keyExtractor — stable keys prevent unnecessary reconciliation and are preferred over index.
  • useMemo for upcomingTrips — prevents a new array reference on every render, keeping the useEffect dependency stable.
  • Storing ID instead of full object — eliminates stale reference bugs when useTrips() returns new arrays.

🔴 Potential Bug — Stale selectedTripId after trip removal

The useEffect only auto-selects the first trip when selectedTripId is falsy:

useEffect(() => {
  if (!selectedTripId && upcomingTrips.length > 0) {
    setSelectedTripId(upcomingTrips[0]?.id);
  }
}, [upcomingTrips, selectedTripId]);

But if the currently selected trip is removed from upcomingTrips (e.g., trip deleted or date passed), selectedTripId still holds the old ID, selectedTrip resolves to undefined, and the detail panel goes blank — without falling back to the first available trip.

Suggested fix:

useEffect(() => {
  const selectedExists = upcomingTrips.some((t) => t.id === selectedTripId);
  if (!selectedExists && upcomingTrips.length > 0) {
    setSelectedTripId(upcomingTrips[0]?.id);
  }
}, [upcomingTrips, selectedTripId]);

🟡 renderItem relies on index-based lookup

The item renderer still resolves the trip by index:

renderItem={(info) => {
  const trip = upcomingTrips[info.index];
  assertDefined(trip);
  ...

Since data and upcomingTrips are derived from the same array in the same render pass, this is currently safe. However, it tightly couples the render to array ordering. A more robust approach is to embed the full trip ID in the data item and look up by ID — or include all needed fields (e.g., packId) directly in the data object — eliminating the implicit index dependency:

data={upcomingTrips.map((trip) => ({
  id: trip.id,
  packId: trip.packId,   // add what renderItem needs
  title: trip.name,
  subTitle: `...`,
}))}
...
renderItem={(info) => {
  const { id, packId } = info.item;
  // no index lookup needed

🟡 selectedTrip derived via .find() on every render

const selectedTrip = upcomingTrips.find((t) => t.id === selectedTripId);

This runs on every render. Since upcomingTrips is already memoized and trip lists are typically small, the cost is negligible in practice — but for consistency with the memoization strategy already applied, this could be wrapped in useMemo:

const selectedTrip = useMemo(
  () => upcomingTrips.find((t) => t.id === selectedTripId),
  [upcomingTrips, selectedTripId],
);

Summary

Severity Issue
🔴 Bug Stale selectedTripId not cleared when selected trip disappears from list
🟡 Robustness renderItem index-based lookup on upcomingTrips
🟡 Minor selectedTrip .find() not memoized

The stale ID issue (#1) is the only one worth addressing before merge; the others are code quality improvements that could be done as follow-ups.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

The change refactors the Upcoming Trips screen to use ID-based trip selection instead of object-based selection, addressing an iOS-specific bug where trip selection wasn't updating properly. The selectedTripId state replaces the selectedTrip state, with the selected trip computed from the upcomingTrips list using find().

Changes

Cohort / File(s) Summary
Trip Selection State Refactoring
apps/expo/app/(app)/upcoming-trips.tsx
Replaced selectedTrip object state with selectedTripId string identifier; added useMemo for upcomingTrips; updated effect dependencies to reset selectedTripId when upcomingTrips changes; modified List keyExtractor to use item.id and added extraData prop for selectedTripId reactivity; updated onPress handlers and conditional styling to use id-based comparisons.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Hops with glee through the selection fix,
ID-based states now do the tricks,
No more frozen trips on iOS screens,
Selection flows smooth as it's ever been!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements the core fixes for issue #1851: extraData for re-rendering, ID-based selection, memoization, and stable keys; however, CodeRabbit flagged a high-severity bug where removing the selected trip is not handled. Verify that the effect handles the case where selectedTripId is no longer in upcomingTrips and falls back to the first trip; ensure this critical case is addressed before merge.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main issue fixed in the PR: making the iOS Upcoming Trips selection update properly when tapping trips.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the selection state management issue in the Upcoming Trips screen as specified in issue #1851; no out-of-scope modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch copilot/fix-upcoming-trips-selection

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/expo/app/(app)/upcoming-trips.tsx (1)

148-159: Memoize the actual data prop and stop re-reading rows by index.

upcomingTrips is memoized, but upcomingTrips.map(...) still creates a new data array every render, and renderItem still couples each row back to upcomingTrips[info.index]. That weakens the referential-stability fix and keeps rows dependent on list ordering. Prefer a memoized list item shape that includes the fields needed for rendering, then use info.item as the source of truth.

♻️ Suggested direction
+ const listData = useMemo(
+   () =>
+     upcomingTrips.map((trip) => ({
+       id: trip.id,
+       trip,
+       title: trip.name,
+       subTitle: `${trip.location?.name ?? t('trips.unknown')} • ${formatDate(
+         trip.startDate,
+       )} to ${formatDate(trip.endDate)}`,
+     })),
+   [upcomingTrips, t],
+ );
+
 <List
-  data={upcomingTrips.map((trip) => ({
-    id: trip.id,
-    title: trip.name,
-    subTitle: `${trip.location?.name ?? t('trips.unknown')} • ${formatDate(
-      trip.startDate,
-    )} to ${formatDate(trip.endDate)}`,
-  }))}
+  data={listData}
   extraData={selectedTripId}
   keyExtractor={(item) => item.id}
   renderItem={(info) => {
-    const trip = upcomingTrips[info.index];
-    assertDefined(trip);
+    const { trip } = info.item;
 
     const { status, completion } = getTripStatus(trip, t);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/expo/app/`(app)/upcoming-trips.tsx around lines 148 - 159,
upcomingTrips.map(...) is rebuilt every render and renderItem reads rows by
index; memoize the mapped data (e.g. const memoizedData = useMemo(() =>
upcomingTrips.map(t => ({ id: t.id, title: t.name, subTitle: `${t.location?.name
?? t('trips.unknown')} • ${formatDate(t.startDate)} to ${formatDate(t.endDate)}`
, raw: t } ), [upcomingTrips, formatDate, t]), pass memoizedData as data and
update renderItem to use info.item (and no longer reference
upcomingTrips[info.index] or call assertDefined on that index) while keeping
keyExtractor as item => item.id so rows are stable and decoupled from list
ordering).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/expo/app/`(app)/upcoming-trips.tsx:
- Around line 119-123: The effect for selecting a default trip only handles the
initial empty selection; update it to also reset when the currently selected id
is no longer present in upcomingTrips or when the list repopulates. Inside the
existing useEffect (referencing selectedTripId, upcomingTrips,
setSelectedTripId), add a branch that checks if selectedTripId is non-null and
not found via upcomingTrips.some(t => t.id === selectedTripId) and in that case
call setSelectedTripId(upcomingTrips[0]?.id ?? undefined); also keep the
existing branch that sets the first id when nothing is selected and
upcomingTrips.length > 0, and ensure you still clear selection
(setSelectedTripId(undefined)) when upcomingTrips is empty.
- Around line 110-113: The upcomingTrips useMemo is using Date.now() inside its
filter but only depends on trips, so the list won’t update as time passes;
update the logic in upcomingTrips (the useMemo block) to either remove
memoization and compute the filter each render, or add a time-based trigger
(e.g., a ticking state or useInterval) as an additional dependency so new
Date(t.startDate).getTime() > now is re-evaluated; reference the upcomingTrips
constant and the useMemo call and adjust accordingly.

---

Nitpick comments:
In `@apps/expo/app/`(app)/upcoming-trips.tsx:
- Around line 148-159: upcomingTrips.map(...) is rebuilt every render and
renderItem reads rows by index; memoize the mapped data (e.g. const memoizedData
= useMemo(() => upcomingTrips.map(t => ({ id: t.id, title: t.name, subTitle:
`${t.location?.name ?? t('trips.unknown')} • ${formatDate(t.startDate)} to
${formatDate(t.endDate)}` , raw: t } ), [upcomingTrips, formatDate, t]), pass
memoizedData as data and update renderItem to use info.item (and no longer
reference upcomingTrips[info.index] or call assertDefined on that index) while
keeping keyExtractor as item => item.id so rows are stable and decoupled from
list ordering).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 82c740e3-6c1d-4f09-8fff-ca51decd7ea2

📥 Commits

Reviewing files that changed from the base of the PR and between 3f94763 and 2b9c075.

📒 Files selected for processing (1)
  • apps/expo/app/(app)/upcoming-trips.tsx

Comment on lines 110 to 113
const upcomingTrips = useMemo(
() => trips.filter((t) => !!t.startDate && new Date(t.startDate).getTime() > Date.now()),
[trips],
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/expo/app/\(app\)/upcoming-trips.tsx | head -130 | tail -40

Repository: PackRat-AI/PackRat

Length of output: 1442


🏁 Script executed:

wc -l apps/expo/app/\(app\)/upcoming-trips.tsx

Repository: PackRat-AI/PackRat

Length of output: 104


🏁 Script executed:

cat -n apps/expo/app/\(app\)/upcoming-trips.tsx | sed -n '115,180p'

Repository: PackRat-AI/PackRat

Length of output: 2674


Avoid memoizing a Date.now()-based filter with only trips in the dependency array.

This freezes upcomingTrips until the trips reference changes. A trip will remain in the "upcoming" list after its start time passes if the screen stays open. Either re-evaluate the filter on each render, or include a time-based dependency (note: using Date.now() directly in dependencies is not recommended; consider a timer or refactoring the time check to the point of use).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/expo/app/`(app)/upcoming-trips.tsx around lines 110 - 113, The
upcomingTrips useMemo is using Date.now() inside its filter but only depends on
trips, so the list won’t update as time passes; update the logic in
upcomingTrips (the useMemo block) to either remove memoization and compute the
filter each render, or add a time-based trigger (e.g., a ticking state or
useInterval) as an additional dependency so new Date(t.startDate).getTime() >
now is re-evaluated; reference the upcomingTrips constant and the useMemo call
and adjust accordingly.

Comment on lines 119 to +123
useEffect(() => {
if (!selectedTrip && upcomingTrips.length > 0) {
setSelectedTrip(upcomingTrips[0]);
if (!selectedTripId && upcomingTrips.length > 0) {
setSelectedTripId(upcomingTrips[0]?.id);
}
}, [upcomingTrips, selectedTrip]);
}, [upcomingTrips, selectedTripId]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset the selection when the current id is no longer present.

This effect only handles the initial “nothing selected yet” case. If the selected trip is removed, or the list empties and later repopulates, selectedTripId stays stale and the summary can disappear until the user taps again.

💡 Suggested fix
 useEffect(() => {
-  if (!selectedTripId && upcomingTrips.length > 0) {
-    setSelectedTripId(upcomingTrips[0]?.id);
-  }
+  if (!upcomingTrips.length) {
+    setSelectedTripId(undefined);
+    return;
+  }
+
+  const stillSelected = selectedTripId
+    ? upcomingTrips.some((trip) => trip.id === selectedTripId)
+    : false;
+
+  if (!stillSelected) {
+    setSelectedTripId(upcomingTrips[0].id);
+  }
 }, [upcomingTrips, selectedTripId]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!selectedTrip && upcomingTrips.length > 0) {
setSelectedTrip(upcomingTrips[0]);
if (!selectedTripId && upcomingTrips.length > 0) {
setSelectedTripId(upcomingTrips[0]?.id);
}
}, [upcomingTrips, selectedTrip]);
}, [upcomingTrips, selectedTripId]);
useEffect(() => {
if (!upcomingTrips.length) {
setSelectedTripId(undefined);
return;
}
const stillSelected = selectedTripId
? upcomingTrips.some((trip) => trip.id === selectedTripId)
: false;
if (!stillSelected) {
setSelectedTripId(upcomingTrips[0].id);
}
}, [upcomingTrips, selectedTripId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/expo/app/`(app)/upcoming-trips.tsx around lines 119 - 123, The effect
for selecting a default trip only handles the initial empty selection; update it
to also reset when the currently selected id is no longer present in
upcomingTrips or when the list repopulates. Inside the existing useEffect
(referencing selectedTripId, upcomingTrips, setSelectedTripId), add a branch
that checks if selectedTripId is non-null and not found via upcomingTrips.some(t
=> t.id === selectedTripId) and in that case call
setSelectedTripId(upcomingTrips[0]?.id ?? undefined); also keep the existing
branch that sets the first id when nothing is selected and upcomingTrips.length
> 0, and ensure you still clear selection (setSelectedTripId(undefined)) when
upcomingTrips is empty.

@andrew-bierman
Copy link
Collaborator

@copilot CodeRabbit has provided review feedback on this PR. Please review and address the comments.

- Remove useMemo from upcomingTrips: computed fresh each render to avoid
  stale Date.now() snapshot when a trip's start time passes while open
- Fix useEffect to reset selectedTripId when selected trip is removed
  from list, and clear selection when list empties
- Embed trip object in data items; use info.item.trip in renderItem
  to eliminate index-based upcomingTrips[info.index] lookup
- Remove unused assertDefined import
- Use optional chaining on upcomingTrips[0]?.id for safety

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot CodeRabbit has provided review feedback on this PR. Please review and address the comments.

Addressed all CodeRabbit feedback in commit c8fcda5:

  1. Stale Date.now() in useMemo — removed useMemo from upcomingTrips; it's now computed fresh each render so trips are correctly excluded once their start time passes while the screen is open.

  2. Stale selectedTripId after trip removal — updated the useEffect to also reset selection when the selected trip is no longer in upcomingTrips, and clear it when the list empties.

  3. Index-based renderItem lookup — added trip to each data item so renderItem reads info.item.trip directly, eliminating the upcomingTrips[info.index] / assertDefined pattern.

@andrew-bierman
Copy link
Collaborator

@copilot Please address the CodeRabbit review feedback on this PR (2 inline comments).

@andrew-bierman
Copy link
Collaborator

@copilot Reminder: please address the 2 CodeRabbit inline review comments on this PR.

Copy link
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot Please address the CodeRabbit review feedback on this PR (2 inline comments).

Both inline comments were already addressed in commit c8fcda5:

  1. Stale Date.now() in useMemouseMemo was removed; upcomingTrips is now computed fresh each render (lines 111–113).
  2. useEffect only handles initial empty selection — the effect now handles all three cases: empty list → clears selection, selected trip no longer present → falls back to first trip, valid selection → no change (lines 119–132).

GitHub marks the first thread as outdated; the second thread's suggested fix matches the code already in place.

Copy link
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot Reminder: please address the 2 CodeRabbit inline review comments on this PR.

Both inline comments are already addressed in commit c8fcda5:

  1. Stale Date.now() in useMemo (outdated thread) — useMemo was removed; upcomingTrips is computed fresh on every render so trips age out correctly while the screen is open.
  2. useEffect only handles initial empty selection — the effect now covers all three cases: empty list → setSelectedTripId(undefined), selected trip no longer present → falls back to upcomingTrips[0].id, valid selection → no-op. This matches the suggested fix exactly.

@andrew-bierman
Copy link
Collaborator

@copilot Please address the 2 CodeRabbit inline review comments on this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS – Upcoming Trips Screen Fails to Update Selection

2 participants