ReviewOS

craft-native/craft

navigation.open takes only a path, so a pushed screen cannot be handed the object its opener already holds

#375
Closed glennmichael123 opened this 9 days ago ·
9 days ago

The ask

navigation.open() takes a path and nothing else:

/** Open `path` as the shell would: a tab, a native screen, or the page pushed. */
open(path: string): boolean

So a list screen cannot hand the pushed screen the object it already holds. A typed second argument — open(path, props), readable as route.props — would close that.

This is ergonomics, not capability. Two channels already work, and I want to be straight about that before asking for a third.

What already works

Query strings survive and merge into route.params. Opening /judges/1?from=feed&name=Probe from one native screen arrives on the next as:

{"params":{"name":"Probe","from":"feed","id":"1","path":"/judges/1?from=feed&name=Probe"}}

craft.snapshots is shared between screens. Writing {from:'reviews', judgeId:1} before open() and reading it on arrival works, and carries structured data rather than strings.

Both verified on an iPhone 17 Pro simulator, craft-native 0.0.187.

Why a props argument would still earn its place

  1. Strings only, through a URL. Anything structured has to be encoded and parsed back, and the types are gone at the boundary — every field arrives as string. A row object with a nested court and a number rating becomes hand-rolled serialisation at every call site.

  2. Path params and query params share one flat namespace. In the payload above, id (from the path) sits beside from and name (from the query) in the same object. A screen mounted at /judges/:id that is also passed ?id= has two different things competing for one key. A separate props channel cannot collide with the route.

  3. The snapshot workaround is a side channel with no tie to the navigation. It is a shared key-value store, so the handoff is "write a key, navigate, hope the next screen reads it before anything else writes it". There is no scoping to the push, nothing clears it when the user goes back instead, and two opens in flight race on the same key. It works, but it is doing a job it was not designed for — it is the offline-snapshot store.

  4. The fallback is an API endpoint per detail screen. Without a props channel the obvious move is to re-fetch. In our app that meant adding GET /api/judges/{id} purely so a pushed screen could name the judge that the list screen was already displaying — a round trip for data that was on screen a moment earlier, and one that fails offline even though the data was in memory. (Query strings or a snapshot would have avoided the common case; the endpoint is still needed for a cold deep link, which is the honest argument for keeping it.)

Shape that would fit

open(path: string, props?: Record<string, unknown>): boolean

with the value surfacing as something distinct from route.params — route.props, untouched by the path matcher, typed by the caller, and dropped when the screen is popped.

Environment

  • craft-native 0.0.187
  • stacks 0.75.187, @stacksjs/stx 0.2.176
  • iPhone 17 Pro simulator, iOS 26.5, Xcode 26.6
9 days ago

Implemented on main in 4a128a5d with host-neutral route props and 1ba3551f with fixture/contract coverage. navigation.open(path, props?) now keeps structured values in route.props, separate from path/query params, on iOS and Android. The issue remains open only for administrative closure.

9 days ago

Resolved on main by 4a128a5 and 1ba3551. navigation.open now carries route props through replacement/push handoffs with native coverage. On main, unreleased.

Sign in to comment on this issue.