@tamer4lynx/tamer-navigation
v0.0.1
Published
Native navigation elements for Lynx — zero-bitmap transitions (slide, fade, modal) via live FrameLayout/UIView stacks.
Readme
@tamer4lynx/tamer-navigation
Framework-agnostic native navigation transport for Tamer/Lynx:
TamerNav(push/pop/popAll/dispatch/update) backed byNativeModules.TamerNavModule- Global events on the root LynxView:
tamer-nav:dispatch,tamer-nav:popped,tamer-nav:transition-end readHydratedStateJson/subscribeHydratedStateJsonandTAMER_NAV_STATE_PROPfor coordinator snapshots
Stack UIs should use native spokes (see @tamer4lynx/tamer-router FileRouter) or your own coordinator on top of this transport.
Choosing a coordinator approach
With @tamer4lynx/tamer-router (recommended): FileRouter handles push/pop wiring, file-based route generation, back gestures, and cross-spoke state bridging. Most apps should use this.
Without tamer-router (manual coordinator): Call TamerNav.push/pop/dispatch/update directly. Listen to tamer-nav:dispatch and tamer-nav:popped global events on the coordinator LynxView and maintain your own route stack. BackHandlerProvider + useBackHandler from @tamer4lynx/tamer-router handle back gestures without the full file router. See packages/example/src/example_stack.tsx for a complete working reference of this pattern.
LynxGroup / LynxViewGroup (required for stack spokes)
TamerNav’s coordinator and stack spoke LynxViews must share one Lynx LynxGroup instance with the root LynxView. They should also use a persistent LynxViewGroup scoped by bundle URL so native template fetches stay tied to the correct source while module singletons remain in the shared runtime group.
- Android: Before
TamerNavHost.attachRoot, configure the root builder with a sharedLynxGroupand a persistentILynxViewGroupfor its bundle URL. SetTamerNavHost.sourceSpokeBuilderso every spoke receives itssrc, resolves the matching view group, and uses the sameLynxGroupreference (seetamer-dev-app/tamer-hosttemplates). - iOS: Set
builder.groupandbuilder.lynxViewGroupon the rootLynxViewbuilder from a shared runtime helper. SetTamerNavHost.configureSpokeBuilderso each spoke receives itssrcand resolves the matchingLynxViewGroup. TamerNavHost.attachRootfails fast (Android and iOS) if the root has no group.
Engine caveat: Lynx’s Android LynxGroup API documents non–single-group IDs as experimental; TamerNav still requires a shared group for this design—treat engine behavior as a known risk.
React: A shared JS context group does not guarantee a single React reconciler or shared React Context across multiple LynxViews; validate app-level assumptions yourself.
State continuity
Important — Lynx engine constraint. A shared
LynxGroup(even withsetEnableJSGroupThread(true)) and a per-URLLynxViewGroupshare the JS thread and template parsing, but not the JS heap / VM context. EachLynxViewinstance receives a fresh JavaScript context, so module-level singletons (zustandcreate(...),reduxcreateStore, plainlet store = ...) are re-evaluated per spoke.This is a property of the upstream Lynx runtime. Audited surfaces:
LynxGroup/LynxGroupBuilder,LynxRuntimeOptions,LynxBackgroundRuntimeOptions,LynxViewBuilder, iOSLynxGroupOption,setBoolConfig/setStringConfigkeys,ILynxViewRuntimeCacheManager, andgetSharedModuleFactory. None expose cross-view JS heap pooling. Patching the Lynx engine (core/runtime/js/) would be required to expose it.
To verify in your build, enable the diagnostic logs shipped with the host/dev-client templates:
- Android:
adb logcat -s TamerHeap— root and spoke print matchinggroup=andviewGroup=identities. - iOS: filter Xcode console for
[TamerHeap]. - App: a
console.log('[TamerHeap] ... module-eval', Math.random())at module top of any singleton store will print once per spoke whenever isolation kicks in.
If identities match but the JS marker prints per-spoke, you are hitting the engine constraint above.
Working today
- React-tree state inside a single
LynxView(Context,useState, etc.) — works as normal. - Pass-through props on
TamerNav.push({ initData, globalProps })— survives navigation since native carries the JSON.
Cross-spoke singleton continuity
Use TamerStateSyncProvider + createTamerStateSync from @tamer4lynx/tamer-router. This is an explicit native-bridge sync that mirrors a JSON snapshot of a store across spokes:
import { create } from 'zustand'
import { createTamerStateSync, TamerStateSyncProvider } from '@tamer4lynx/tamer-router'
export const useDemoStore = create<DemoState>(...)
const demoSync = createTamerStateSync('demo-store', {
getState: () => useDemoStore.getState(),
subscribe: (listener) => useDemoStore.subscribe(listener),
hydrate: (json) => useDemoStore.setState(JSON.parse(json)),
})
// Wrap the FileRouter root once per LynxView entry:
<TamerStateSyncProvider syncs={[demoSync]}>{...}</TamerStateSyncProvider>
// `providers` is a deprecated alias for `syncs`; prefer `syncs`.The same sync object goes in every entry that needs to read/write the store. Bridge cost is one JSON round-trip per setState.
Native transition end
After stack push/pop animations finish, the host emits tamer-nav:transition-end on the coordinator LynxView with { screenId, animated }. screenId is the top spoke; it is empty when the stack is cleared (coordinator only). Use subscribeTamerNavTransitionEnd from this package, or useLynxGlobalEventListener(TAMER_NAV_TRANSITION_END_EVENT, …), to defer heavy UI without coupling to the router.
Android emits after each spoke enter animation ends (onCreateAnimation). When the last spoke is removed there is no enter animation on the coordinator; the package emits after a 280ms delay so it stays aligned with the XML transition duration. iOS uses UINavigationControllerDelegate.navigationController(_:didShow:animated:).
Spoke shell color (globalProps.contentBackgroundColor)
Before the spoke Lynx bundle paints, native containers can flash the wrong color. Pass contentBackgroundColor on TamerNav.push → globalProps as a hex string (#RRGGBB or #AARRGGBB). Android/iOS read this key and tint the fragment/VC root and LynxView; if missing, they fall back to the platform theme background (colorBackground / UIColor.systemBackground), not pure black.
@tamer4lynx/tamer-router FileRouter merges theme.background into globalProps for coordinator native pushes when theme colors are available.
<overlay> (unchanged)
The overlay element remains for dialogs and similar UI. See native sources under overlay/ and TamerNavPageOverlay.
