Next.js + Capacitor 8 iOS: React state (messages, user data) lost on every app resume — WKWebView issue? #8433
Replies: 1 comment
-
|
This is almost certainly WKWebView killing your web content process in the background, not a Capacitor or Supabase issue. It's a well-documented but annoying iOS behaviour, and the symptoms match exactly. What's actually happeningWhen your app goes to background, iOS puts pressure on the WebKit content process ( Your The giveaway diagnostic: on every "resume" where state is lost, look at your server access logs. You should see a fresh request for your HTML document (and a bunch of chunks) at the moment the user foregrounds the app. If you see that, it's process termination — not Supabase, not React, not your auth refresh cadence. How to confirm it in 30 secondsAdd this before any React rendering, in console.log('[cold-boot]', Date.now(), performance.timing?.navigationStart)
window.addEventListener('pagehide', (e) => {
console.log('[pagehide]', { persisted: e.persisted })
})
window.addEventListener('pageshow', (e) => {
console.log('[pageshow]', { persisted: e.persisted })
})From Safari's Web Inspector attached to the device, reproduce the bug. If The only fixes that actually work1. Don't live in remote URL mode for a long-running stateful app. This is the real answer for your use case. Capacitor's remote URL mode was designed for hybrid shells around mostly-idempotent content. For a social app with live messages and per-user state, either:
2. Treat every resume as "the web app might have restarted" If staying in remote URL mode is non-negotiable:
// hooks/useHydratedState.ts
import { Preferences } from '@capacitor/preferences'
export function useHydratedState<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(initial)
// Synchronous-ish rehydrate on mount
useEffect(() => {
Preferences.get({ key }).then(({ value: v }) => {
if (v) setValue(JSON.parse(v))
})
}, [key])
// Persist on change
useEffect(() => {
Preferences.set({ key, value: JSON.stringify(value) })
}, [key, value])
return [value, setValue] as const
}Don't put 3. React to process termination explicitly iOS fires // ios/App/App/AppDelegate.swift or a custom plugin
extension CAPBridgeViewController {
override open func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
// Optional: show a "reconnecting..." overlay, log telemetry, force a reload path
webView.reload()
}
}The default Capacitor behaviour reloads; overriding it lets you show UI or flush logs. More importantly, knowing this event exists helps you see the bug in crash logs — it's the "why did my users see blank screens" signal. 4. Stop fighting the symptoms Your "3-wave recovery" and tuned auth token refresh timing are chasing a different model of the bug. Auth token refresh at 150ms doesn't help because React state was already zero at 0ms. Once (1) or (2) is in place, you can remove the whole recovery-wave apparatus — reconcile-on-resume becomes a simple re-fetch, not a race against state restoration. Why this isn't a Supabase issueSupabase Realtime re-subscribes fine; your logs confirm TL;DR
Happy to look at a minimal repro if you put one together. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
BODY:
Environment
Problem
We have a production social app running via Capacitor in Remote URL mode (the WebView loads
https://our-app.vercel.app). The core issue: every time the user minimizes and reopens the app, React state is lost — chat messages disappear, karma points reset to 0, VIP access is revoked temporarily.This has been happening for weeks despite extensive debugging. The app works perfectly on first load and during active use, but any background/foreground cycle causes data loss.
What we've tried (and what didn't work)
App.addListener("appStateChange")+visibilitychange— We trigger auth token refresh and data re-fetch on resume. The events fire correctly, but the re-fetch responses arrive AFTER React state has already been cleared.Module-level caches — We cache critical data (conversations, unread counts) at module level (outside React components) so they survive re-renders. This works for some hooks but not all.
subscribeWithRecovery()— We built a wrapper around Supabase Realtime.subscribe()that auto-reconnects onCHANNEL_ERROR. Channels do reconnect, but the associated React state is empty.Reducing auth recovery dispatches — We went from 3-wave recovery (300ms, 1s, 2.5s) to a single dispatch at 150ms. Reduced the cascade but didn't fix the core issue.
getSession()instead ofgetUser()—getUser()makes a server call that fails during the brief token-refresh window on resume. Switching to localgetSession()helped for auth checks, but data still disappears.Specific symptom
The same happens for:
Key question
Is there a known pattern for preserving React state across iOS background/foreground cycles in Capacitor Remote URL mode?
Specifically:
localStorageand restore it on mount, or is there a cleaner approach?Relevant code
Our recovery flow in the Supabase client singleton:
Our hooks listen via
useAuthRecovery()which fires the callback. But by the time the callback runs and data is fetched, the UI has already flashed empty.System info
Any help or pointers to similar resolved issues would be greatly appreciated!
Beta Was this translation helpful? Give feedback.
All reactions