Summary
apps/mobile/lib/FrappeNativeProvider.tsx passes a brand-new tokenParams object literal into <FrappeProvider> on every render:
<FrappeProvider
url={siteInfo?.url}
tokenParams={{
type: 'Bearer',
useToken: true,
token: getAccessToken,
}}
siteName={siteInfo?.sitename}
...
>
frappe-react-sdk's FrappeProvider builds its entire config - including the Socket.IO connection - inside a useMemo keyed on tokenParams by object reference (src/lib/index.tsx). Since this object is recreated on every render, the memo reruns and opens a brand-new SocketIO connection every time, with no cleanup of the previous connection. The abandoned connection is simply left running (its own reconnection/heartbeat timers keep going) rather than closed.
Why this compounds over time, not just at startup
FrappeNativeProvider is a plain function component with no React.memo, so it re-renders whenever its parent (app/[site_id]/_layout.tsx) re-renders - which happens routinely, e.g. via that layout's own useNetworkState() hook firing on ordinary mobile connectivity changes (WiFi signal strength, cell handoff, foreground/background). Each such re-render creates a fresh tokenParams object, which reruns the SDK's useMemo, which opens another orphaned socket. On a phone this isn't rare - it can happen repeatedly for the entire time the app stays open, matching real user reports of reliability getting steadily worse the longer a session is left open, not just an issue at launch.
Evidence from a real production deployment
Analyzing live nginx access logs of real Raven Mobile (iOS) traffic over ~9 hours / 90 sessions: every mobile Engine.IO session died within ~1 second via 400 session ID unknown, immediately followed by a new session - during app bootstrap specifically, 3 separate overlapping sessions were created within ~1.3 seconds. This matches exactly what the mechanism above predicts.
Why this doesn't show up in typical web usage
This only affects consumers who pass tokenParams (token-based auth) to FrappeProvider - which a mobile app must do, since it has no shared browser cookie jar. Most web usage relies on session cookies instead, never sets tokenParams, so it stays undefined (a stable reference) and this code path never fires. It's specifically a mobile-app-shaped bug in a library mostly exercised by web usage.
Suggested fix
Memoize tokenParams in FrappeNativeProvider.tsx, keyed on the actual stable values, instead of constructing it inline every render:
const tokenParams = useMemo(() => ({
type: 'Bearer' as const,
useToken: true,
token: getAccessToken,
}), [getAccessToken])
and pass tokenParams={tokenParams} instead of the inline literal. getAccessToken is already stable (wrapped in useCallback(..., []) in _layout.tsx), so this fully stabilizes the reference across re-renders. Opening a PR with this exact change.
Environment
raven develop/main branch (confirmed present as of 2026-07-24)
frappe-react-sdk latest main (confirmed present)
- Affects
apps/mobile/lib/FrappeNativeProvider.tsx
Summary
apps/mobile/lib/FrappeNativeProvider.tsxpasses a brand-newtokenParamsobject literal into<FrappeProvider>on every render:frappe-react-sdk'sFrappeProviderbuilds its entire config - including the Socket.IO connection - inside auseMemokeyed ontokenParamsby object reference (src/lib/index.tsx). Since this object is recreated on every render, the memo reruns and opens a brand-newSocketIOconnection every time, with no cleanup of the previous connection. The abandoned connection is simply left running (its own reconnection/heartbeat timers keep going) rather than closed.Why this compounds over time, not just at startup
FrappeNativeProvideris a plain function component with noReact.memo, so it re-renders whenever its parent (app/[site_id]/_layout.tsx) re-renders - which happens routinely, e.g. via that layout's ownuseNetworkState()hook firing on ordinary mobile connectivity changes (WiFi signal strength, cell handoff, foreground/background). Each such re-render creates a freshtokenParamsobject, which reruns the SDK'suseMemo, which opens another orphaned socket. On a phone this isn't rare - it can happen repeatedly for the entire time the app stays open, matching real user reports of reliability getting steadily worse the longer a session is left open, not just an issue at launch.Evidence from a real production deployment
Analyzing live nginx access logs of real Raven Mobile (iOS) traffic over ~9 hours / 90 sessions: every mobile Engine.IO session died within ~1 second via
400 session ID unknown, immediately followed by a new session - during app bootstrap specifically, 3 separate overlapping sessions were created within ~1.3 seconds. This matches exactly what the mechanism above predicts.Why this doesn't show up in typical web usage
This only affects consumers who pass
tokenParams(token-based auth) toFrappeProvider- which a mobile app must do, since it has no shared browser cookie jar. Most web usage relies on session cookies instead, never setstokenParams, so it staysundefined(a stable reference) and this code path never fires. It's specifically a mobile-app-shaped bug in a library mostly exercised by web usage.Suggested fix
Memoize
tokenParamsinFrappeNativeProvider.tsx, keyed on the actual stable values, instead of constructing it inline every render:and pass
tokenParams={tokenParams}instead of the inline literal.getAccessTokenis already stable (wrapped inuseCallback(..., [])in_layout.tsx), so this fully stabilizes the reference across re-renders. Opening a PR with this exact change.Environment
ravendevelop/main branch (confirmed present as of 2026-07-24)frappe-react-sdklatest main (confirmed present)apps/mobile/lib/FrappeNativeProvider.tsx