Skip to main content
This example demonstrates how to use Stan.js in React Native applications with MMKV storage for fast, efficient persistence.

Why MMKV?

MMKV is significantly faster than AsyncStorage:

Lightning Fast

Up to 30x faster than AsyncStorage

Synchronous

No async/await needed for reads

Type Safe

Full TypeScript support built-in

Installation

Install Stan.js and react-native-mmkv:
npm install stan-js react-native-mmkv
For React Native, Stan.js automatically uses the MMKV storage adapter when you import from stan-js/storage.

Basic Setup

Create a store with MMKV persistence:
store.ts
import { createStore } from 'stan-js'
import { storage } from 'stan-js/storage'

type User = {
    id: string
    name: string
    avatar: string
}

export const { useStore, getState, actions, reset } = createStore({
    // Persisted to MMKV
    user: storage<User | null>(null, {
        storageKey: 'user',
    }),
    token: storage<string | null>(null, {
        storageKey: 'auth-token',
    }),
    settings: storage({
        theme: 'light' as 'light' | 'dark',
        notifications: true,
        language: 'en',
    }, {
        storageKey: 'app-settings',
    }),
    // Not persisted
    isOnline: true,
})

React Native Component

App.tsx
import { View, Text, Button, Switch } from 'react-native'
import { useStore } from './store'

export const App = () => {
    const { user, settings, setSettings } = useStore()

    const toggleTheme = () => {
        setSettings(prev => ({
            ...prev,
            theme: prev.theme === 'light' ? 'dark' : 'light',
        }))
    }

    return (
        <View style={{ flex: 1, backgroundColor: settings.theme === 'dark' ? '#000' : '#fff' }}>
            <Text>Welcome {user?.name ?? 'Guest'}!</Text>
            
            <View>
                <Text>Dark Mode</Text>
                <Switch
                    value={settings.theme === 'dark'}
                    onValueChange={toggleTheme}
                />
            </View>
            
            <View>
                <Text>Notifications</Text>
                <Switch
                    value={settings.notifications}
                    onValueChange={enabled =>
                        setSettings(prev => ({ ...prev, notifications: enabled }))
                    }
                />
            </View>
        </View>
    )
}

Custom MMKV Instance

Create a custom MMKV instance with encryption:
customStorage.ts
import { MMKV } from 'react-native-mmkv'
import { createStorage } from 'stan-js/storage'

// Create encrypted MMKV instance
const mmkvInstance = new MMKV({
    id: 'user-data',
    encryptionKey: 'your-encryption-key',
})

// Create storage with custom MMKV instance
export const secureStorage = createStorage({
    mmkvInstance,
})
Use the secure storage in your store:
store.ts
import { createStore } from 'stan-js'
import { secureStorage } from './customStorage'

export const { useStore } = createStore({
    sensitiveData: secureStorage({
        creditCard: '',
        ssn: '',
    }),
})

Authentication Example

auth.ts
import { createStore } from 'stan-js'
import { storage } from 'stan-js/storage'

type AuthState = {
token: string | null
user: {
    id: string
    email: string
    name: string
} | null
isAuthenticated: boolean
}

export const { useStore, getState, actions } = createStore({
token: storage<string | null>(null, {
    storageKey: 'auth-token',
}),
user: storage<AuthState['user']>(null, {
    storageKey: 'user-data',
}),
get isAuthenticated() {
    return this.token !== null && this.user !== null
},
})

export const login = async (email: string, password: string) => {
try {
    const response = await fetch('https://api.example.com/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
    })

    const { token, user } = await response.json()

    actions.setToken(token)
    actions.setUser(user)

    return true
} catch (error) {
    console.error('Login failed:', error)
    return false
}
}

export const logout = () => {
actions.setToken(null)
actions.setUser(null)
}

Storage Configuration

Multiple MMKV Instances

Organize data into separate MMKV instances:
storage.ts
import { MMKV } from 'react-native-mmkv'
import { createStorage } from 'stan-js/storage'

// User data storage
const userMMKV = new MMKV({ id: 'user-storage' })
export const userStorage = createStorage({ mmkvInstance: userMMKV })

// Cache storage (can be cleared)
const cacheMMKV = new MMKV({ id: 'cache-storage' })
export const cacheStorage = createStorage({ mmkvInstance: cacheMMKV })

// Secure storage with encryption
const secureMMKV = new MMKV({
    id: 'secure-storage',
    encryptionKey: 'your-encryption-key',
})
export const secureStorage = createStorage({ mmkvInstance: secureMMKV })
store.ts
import { createStore } from 'stan-js'
import { userStorage, cacheStorage, secureStorage } from './storage'

export const { useStore } = createStore({
    user: userStorage(null),
    recentSearches: cacheStorage([]),
    credentials: secureStorage({ pin: '' }),
})

Custom Serialization

Handle complex data types:
import { createStorage } from 'stan-js/storage'

export const dateStorage = createStorage({
    serialize: (value: Date) => value.toISOString(),
    deserialize: (value: string) => new Date(value),
})

export const { useStore } = createStore({
    lastLogin: dateStorage(new Date()),
})

Performance Tips

// Bad: Multiple storage writes
actions.setName('John')
actions.setEmail('[email protected]')
actions.setAge(30)

// Good: Single storage write
actions.setUser({
    name: 'John',
    email: '[email protected]',
    age: 30,
})
// Only persist what's needed
const { useStore } = createStore({
    user: storage({ name: '', email: '' }), // Persisted
    isLoading: false, // Not persisted - ephemeral state
    error: null, // Not persisted
})
import { MMKV } from 'react-native-mmkv'

const cache = new MMKV({ id: 'cache' })

// Clear all cached data
export const clearCache = () => {
    cache.clearAll()
}

// Clear specific keys
export const clearSearchHistory = () => {
    cache.delete('search-history')
}
import { storage } from 'stan-js/storage'

type Settings = {
    theme: 'light' | 'dark'
    version: number
}

export const { useStore } = createStore({
    settings: storage<Settings>(
        { theme: 'light', version: 1 },
        {
            deserialize: (value) => {
                const parsed = JSON.parse(value)
                
                // Migrate old version
                if (!parsed.version || parsed.version < 2) {
                    return {
                        ...parsed,
                        theme: parsed.darkMode ? 'dark' : 'light',
                        version: 2,
                    }
                }
                
                return parsed
            },
        }
    ),
})

Common Patterns

App Theme

theme.ts
import { createStore } from 'stan-js'
import { storage } from 'stan-js/storage'
import { useColorScheme } from 'react-native'

export const { useStore } = createStore({
    theme: storage<'light' | 'dark' | 'auto'>('auto'),
})

export const useTheme = () => {
    const { theme } = useStore()
    const systemTheme = useColorScheme()

    const activeTheme = theme === 'auto' ? systemTheme : theme

    return {
        theme: activeTheme,
        isDark: activeTheme === 'dark',
    }
}

Offline Queue

offline.ts
import { createStore } from 'stan-js'
import { storage } from 'stan-js/storage'
import NetInfo from '@react-native-community/netinfo'

type QueuedRequest = {
    id: string
    url: string
    method: string
    body: any
    timestamp: number
}

export const { useStore, actions } = createStore({
    queue: storage<QueuedRequest[]>([]),
    isOnline: true,
})

// Listen to network status
NetInfo.addEventListener(state => {
    actions.setIsOnline(state.isConnected ?? false)
    
    if (state.isConnected) {
        processQueue()
    }
})

const processQueue = async () => {
    const { queue } = getState()
    
    for (const request of queue) {
        try {
            await fetch(request.url, {
                method: request.method,
                body: JSON.stringify(request.body),
            })
            
            // Remove from queue
            actions.setQueue(prev => prev.filter(r => r.id !== request.id))
        } catch (error) {
            console.error('Failed to process request:', error)
        }
    }
}

Next Steps

Basic Counter

Learn Stan.js fundamentals

Async Data

Handle async operations

Storage API

Explore storage options

React Native Docs

Official React Native documentation

Build docs developers (and LLMs) love