Vue NativeVue Native
Guide
Components
Composables
Navigation
  • iOS
  • Android
  • macOS
GitHub
Guide
Components
Composables
Navigation
  • iOS
  • Android
  • macOS
GitHub
  • Getting Started

    • Introduction
    • Installation
    • Your First App
    • Project Structure
  • Core Concepts

    • Components
    • Styling
    • Navigation
    • Native Modules
    • Native Code Blocks
    • Hot Reload
  • Advanced

    • Error Handling
    • Accessibility
    • TypeScript
    • Performance
    • Shared Element Transitions
    • Testing
    • Security
    • Debugging
    • Teleport
    • Forms and v-model
  • Integration Guides

    • State Management
    • Deep Linking & Universal Links
    • State Persistence
    • Push Notifications
    • Error Reporting & Monitoring
  • Tooling

    • Managed Workflow
    • VS Code Extension
    • Neovim Plugin
  • Building & Releasing

    • Building for Release
    • Deployment & App Store Submission
  • Reference

    • Migration & Upgrade Guide
    • Known Limitations & Platform Differences
    • Troubleshooting

State Persistence

Vue Native provides several mechanisms for persisting state across app launches: automatic navigation state restoration, key-value storage, encrypted storage for sensitive data, and a local SQLite database for structured data.

Navigation State Persistence

The Vue Native router can automatically save and restore the navigation stack so users return to the exact screen they left.

Configuration

Enable state persistence by setting persistState: true in your router options:

import { createApp } from '@thelacanians/vue-native-runtime'
import { createRouter } from '@thelacanians/vue-native-navigation'
import App from './App.vue'
import Home from './screens/Home.vue'
import Profile from './screens/Profile.vue'
import Settings from './screens/Settings.vue'

const router = createRouter({
  routes: [
    { name: 'home', component: Home },
    { name: 'profile', component: Profile },
    { name: 'settings', component: Settings },
  ],
  persistState: true,
  // Optional: customize the storage key (defaults to '__vue_native_nav_state__')
  persistKey: 'my_app_nav_state',
})

createApp(App).use(router).start()

How It Works

When persistState is enabled, the router:

  1. On creation -- Reads the saved state from AsyncStorage using the persistKey and calls restoreState() to rebuild the navigation stack.
  2. On navigation changes -- Watches the stack for changes and saves the current state to AsyncStorage, debounced by 300ms to avoid excessive writes.

The state is serialized as JSON with the following structure:

interface NavigationState {
  stack: Array<{ name: string; params: Record<string, any> }>
  index: number
}

What Gets Saved

SavedNot Saved
Screen namesComponent instances
Route params (serializable values)Functions in params
Stack order and current indexSymbols in params
Reactive state inside components

Warning

Only JSON-serializable values in route params are persisted. If a route param contains a function or a Symbol, it will be lost on restore. In development mode, the router logs a warning for each non-serializable param:

[vue-native/navigation] Route "profile" has non-serializable param "onUpdate" (function).
This value will be lost during state persistence.

Manual State Control

You can also use getState() and restoreState() directly for custom persistence strategies:

import { useRouter } from '@thelacanians/vue-native-navigation'

const router = useRouter()

// Snapshot the current navigation state
const state = router.getState()
// { stack: [{ name: 'home', params: {} }, { name: 'profile', params: { id: '42' } }], index: 1 }

// Restore a previously saved state
router.restoreState(state)

restoreState() validates every route name against the registered routes. If a route name no longer exists (for example, after an app update that renamed a screen), the router resets to the initial route and logs a warning:

[vue-native/navigation] Route "old-screen" not found in restoreState, resetting to initial

If the state object is invalid (null, not an array, or empty), restoreState() is a no-op:

[vue-native/navigation] Invalid state, ignoring restoreState

App State Persistence

Beyond navigation, you will often need to persist user preferences, tokens, or application data. Vue Native provides three storage composables at different levels of complexity.

Key-Value Storage with useAsyncStorage

useAsyncStorage provides simple string-based key-value storage backed by UserDefaults (iOS) and SharedPreferences (Android). It is ideal for user preferences, feature flags, and small pieces of data.

<script setup>
import { ref, onMounted } from '@thelacanians/vue-native-runtime'
import { useAsyncStorage } from '@thelacanians/vue-native-runtime'

const storage = useAsyncStorage()
const theme = ref('light')

onMounted(async () => {
  const saved = await storage.getItem('theme')
  if (saved) theme.value = saved
})

async function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
  await storage.setItem('theme', theme.value)
}
</script>

API:

MethodReturn TypeDescription
getItem(key)Promise<string | null>Read a value by key
setItem(key, value)Promise<void>Write a string value
removeItem(key)Promise<void>Delete a key
getAllKeys()Promise<string[]>List all stored keys
clear()Promise<void>Remove all entries

Tips

Write operations (setItem, removeItem) are serialized per key. If you call setItem('theme', 'dark') and setItem('theme', 'light') in quick succession, they execute in order -- the second write waits for the first to complete. This prevents race conditions from concurrent access.

All values must be strings. For objects, serialize with JSON.stringify and deserialize with JSON.parse:

// Save an object
await storage.setItem('preferences', JSON.stringify({ fontSize: 16, lang: 'en' }))

// Load an object
const raw = await storage.getItem('preferences')
const prefs = raw ? JSON.parse(raw) : { fontSize: 14, lang: 'en' }

Secure Storage with useSecureStorage

useSecureStorage stores data in the iOS Keychain or Android EncryptedSharedPreferences. Use it for auth tokens, API keys, and any sensitive information.

<script setup>
import { useSecureStorage } from '@thelacanians/vue-native-runtime'

const secureStorage = useSecureStorage()

async function saveToken(token: string) {
  await secureStorage.setItem('auth_token', token)
}

async function getToken(): Promise<string | null> {
  return secureStorage.getItem('auth_token')
}

async function logout() {
  await secureStorage.removeItem('auth_token')
}
</script>

API:

MethodReturn TypeDescription
getItem(key)Promise<string | null>Read a secure value
setItem(key, value)Promise<void>Write a secure value
removeItem(key)Promise<void>Delete a secure entry
clear()Promise<void>Remove all secure entries

Warning

Secure storage is slower than useAsyncStorage because of the encryption overhead. Only use it for data that genuinely requires protection. User preferences and UI state should use useAsyncStorage instead.

Structured Data with useDatabase

useDatabase provides reactive SQLite access for structured or relational data. The database opens on first use and auto-closes when the component unmounts.

<script setup>
import { ref, onMounted } from '@thelacanians/vue-native-runtime'
import { useDatabase } from '@thelacanians/vue-native-runtime'

interface Todo {
  id: number
  title: string
  done: number
}

const db = useDatabase('todos')
const items = ref<Todo[]>([])

onMounted(async () => {
  await db.execute(
    'CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY, title TEXT, done INTEGER DEFAULT 0)'
  )
  items.value = await db.query<Todo>('SELECT * FROM todos ORDER BY id DESC')
})

async function addTodo(title: string) {
  await db.execute('INSERT INTO todos (title) VALUES (?)', [title])
  items.value = await db.query<Todo>('SELECT * FROM todos ORDER BY id DESC')
}

async function toggleTodo(id: number, done: boolean) {
  await db.execute('UPDATE todos SET done = ? WHERE id = ?', [done ? 1 : 0, id])
  items.value = await db.query<Todo>('SELECT * FROM todos ORDER BY id DESC')
}
</script>

API:

MethodReturn TypeDescription
execute(sql, params?)Promise<ExecuteResult>Run an INSERT, UPDATE, or DELETE statement
query<T>(sql, params?)Promise<T[]>Run a SELECT and return rows
transaction(callback)Promise<void>Execute multiple statements atomically
close()Promise<void>Manually close the database
isOpenRef<boolean>Whether the database is currently open

Transactions ensure atomicity -- if any statement fails, all changes are rolled back:

await db.transaction(async ({ execute }) => {
  await execute('INSERT INTO todos (title) VALUES (?)', ['Buy groceries'])
  await execute('INSERT INTO todos (title) VALUES (?)', ['Walk the dog'])
  // If either INSERT fails, both are rolled back
})

State Management Patterns

Vue Native does not ship a dedicated state management library. Instead, use Vue 3's built-in reactivity with reactive() and provide/inject to share state across screens.

Reactive Store Pattern

Create a composable that returns a reactive store and provide it at the app level:

// stores/auth.ts
import { reactive, readonly } from '@thelacanians/vue-native-runtime'
import { useSecureStorage } from '@thelacanians/vue-native-runtime'

interface AuthState {
  isLoggedIn: boolean
  user: { id: string; name: string } | null
  token: string | null
}

const state = reactive<AuthState>({
  isLoggedIn: false,
  user: null,
  token: null,
})

const secureStorage = useSecureStorage()

export function useAuthStore() {
  async function login(email: string, password: string) {
    // Call your API
    const response = await fetch('https://api.example.com/login', {
      method: 'POST',
      body: JSON.stringify({ email, password }),
    })
    const data = await response.json()

    state.token = data.token
    state.user = data.user
    state.isLoggedIn = true

    // Persist the token securely
    await secureStorage.setItem('auth_token', data.token)
  }

  async function logout() {
    state.token = null
    state.user = null
    state.isLoggedIn = false
    await secureStorage.removeItem('auth_token')
  }

  async function restoreSession() {
    const token = await secureStorage.getItem('auth_token')
    if (token) {
      state.token = token
      state.isLoggedIn = true
      // Optionally fetch user profile
    }
  }

  return {
    state: readonly(state),
    login,
    logout,
    restoreSession,
  }
}

Sharing State Between Screens

Use provide at the root component and inject in child screens:

<!-- App.vue -->
<script setup>
import { provide } from '@thelacanians/vue-native-runtime'
import { useAuthStore } from './stores/auth'

const authStore = useAuthStore()
provide('auth', authStore)

// Restore session on app launch
authStore.restoreSession()
</script>

<template>
  <RouterView />
</template>
<!-- screens/Profile.vue -->
<script setup>
import { inject } from '@thelacanians/vue-native-runtime'

const auth = inject('auth')
</script>

<template>
  <VView :style="{ flex: 1, padding: 20 }">
    <VText :style="{ fontSize: 20 }">
      Welcome, {{ auth.state.user?.name ?? 'Guest' }}
    </VText>
    <VButton :onPress="auth.logout">
      <VText>Log Out</VText>
    </VButton>
  </VView>
</template>

Persisting User Preferences

A common pattern combines useAsyncStorage with a reactive store for preferences that survive app restarts:

// stores/preferences.ts
import { reactive, watch } from '@thelacanians/vue-native-runtime'
import { useAsyncStorage } from '@thelacanians/vue-native-runtime'

const STORAGE_KEY = 'user_preferences'

interface Preferences {
  theme: 'light' | 'dark'
  fontSize: number
  notificationsEnabled: boolean
}

const defaults: Preferences = {
  theme: 'light',
  fontSize: 16,
  notificationsEnabled: true,
}

const state = reactive<Preferences>({ ...defaults })
const storage = useAsyncStorage()
let initialized = false

export function usePreferences() {
  async function load() {
    if (initialized) return
    const raw = await storage.getItem(STORAGE_KEY)
    if (raw) {
      try {
        Object.assign(state, JSON.parse(raw))
      } catch {
        // Corrupted data -- use defaults
      }
    }
    initialized = true
  }

  // Auto-save when any preference changes
  watch(state, async () => {
    if (!initialized) return
    await storage.setItem(STORAGE_KEY, JSON.stringify(state))
  }, { deep: true })

  return { preferences: state, load }
}

Use it from your root component:

<!-- App.vue -->
<script setup>
import { provide } from '@thelacanians/vue-native-runtime'
import { usePreferences } from './stores/preferences'

const { preferences, load } = usePreferences()
provide('preferences', preferences)

load() // Restore on launch
</script>

Choosing the Right Storage

Use CaseRecommended StorageComposable
User preferences (theme, language)AsyncStorageuseAsyncStorage
Auth tokens, API keysSecure StorageuseSecureStorage
Navigation stackBuilt-in persistencepersistState: true
Structured app data (todos, messages)SQLiteuseDatabase
Temporary UI state (form inputs)Vue ref/reactiveNone needed

See Also

  • useAsyncStorage reference
  • Navigation guide
  • Navigation params
Edit this page
Last Updated: 2/28/26, 11:24 PM
Contributors: Abdul Hamid, Claude Opus 4.6
Prev
Deep Linking & Universal Links
Next
Push Notifications