Framework7 Storage and State Management β Data Persistence and App State
In this tutorial, you will learn about Framework7 Storage and State Management. We cover key concepts, practical examples, and best practices to help you master this topic.
Framework7 provides reactive state management through the Store, a localStorage wrapper for data persistence, and app-level data sharing between views, pages, and components.
What You'll Learn
- Framework7 Store reactive state
- localStorage wrapper (app.data)
- Sharing data between views
- Form state persistence
- State restoration on app restart
Why It Matters
Mobile apps need persistent state β user preferences, form drafts, cached data, and page state. Framework7's Store provides reactive state that triggers UI updates, while localStorage integration persists across sessions.
Real-World Use
A todo app where the Store manages the todo list reactively, form drafts auto-save to localStorage, user preferences persist, and the app restores the last-visited page on restart.
State Architecture
flowchart TD
A[State Management] --> B[F7 Store]
A --> C[app.data]
A --> D[localStorage]
B --> E[Reactive State]
B --> F[Computed Values]
B --> G[Actions]
C --> H[JSON Persistence]
D --> I[Form Drafts]
D --> J[User Preferences]
A --> K[State Restoration]
style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px
Framework7 Store
// Define a store
var store = app.store.create({
state: {
users: [],
currentUser: null,
loading: false,
error: null,
notifications: 0,
theme: 'light',
settings: {
pushEnabled: true,
darkMode: false,
fontSize: 16
}
},
getters: {
// Computed values
activeUsers: function(state) {
return state.users.filter(function(u) { return u.active; });
},
userCount: function(state) {
return state.users.length;
},
notificationCount: function(state) {
return state.notifications;
},
isDarkMode: function(state) {
return state.settings.darkMode;
}
},
actions: {
// Async actions
loadUsers: function(context) {
context.state.loading = true;
return fetch('/api/users')
.then(function(r) { return r.json(); })
.then(function(users) {
context.state.users = users;
context.state.loading = false;
});
},
addUser: function(context, user) {
context.state.users.push(user);
context.dispatch('saveToLocalStorage');
},
removeUser: function(context, userId) {
context.state.users = context.state.users.filter(function(u) {
return u.id !== userId;
});
},
setTheme: function(context, theme) {
context.state.theme = theme;
context.state.settings.darkMode = theme === 'dark';
localStorage.setItem('theme', theme);
},
saveToLocalStorage: function(context) {
localStorage.setItem('users-data', JSON.stringify(context.state.users));
}
}
});
// Using the store in pages
$$(document).on('page:init', '.page[data-name="users"]', function() {
store.dispatch('loadUsers');
});
Expected output: The Store manages application state reactively. Actions mutate state, getters compute derived values, and components react to state changes automatically.
Consuming Store in Components
// Subscribe to state changes
var userId = store.getters.userCount;
console.log('User count:', userId);
// Direct state access
console.log('Loading:', store.state.loading);
// Watch for changes
var unwatch = store.watch('state.users', function(newVal, oldVal) {
console.log('Users changed from', oldVal, 'to', newVal);
// Update UI
renderUsers(newVal);
});
// Watch specific getter
store.watch('getters.isDarkMode', function(isDark) {
if (isDark) {
$$('body').addClass('dark-theme');
} else {
$$('body').removeClass('dark-theme');
}
});
// Unwatch when done
// unwatch();
// Reactive binding in templates
<div class="list">
<div class="block-header">
Users <span class="badge">{{store.getters.userCount}}</span>
</div>
<ul>
{{#each store.state.users}}
<li>
<a href="/user/{{id}}/" class="item-link item-content">
<div class="item-inner">
<div class="item-title">{{name}}</div>
</div>
</a>
</li>
{{/each}}
</ul>
</div>
Expected output: Components subscribe to Store state and update automatically when state changes. The watch function triggers callbacks on specific state changes.
App-level State (app.data)
// app.data is a simple persistent data store
// It syncs to localStorage automatically
// Set data
app.data.users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
app.data.settings = {
lastPage: '/users/',
scrollPosition: 245
};
// Get data
var users = app.data.users;
console.log('Saved users:', users);
// Save explicitly
app.dataSave();
// Load data
app.dataLoad();
// Check if data exists
if (app.data.users && app.data.users.length) {
console.log('Data loaded from localStorage');
} else {
console.log('No saved data');
}
// Clear data
delete app.data.users;
app.dataSave();
Expected output: app.data persists to localStorage automatically. Data survives page refreshes and is available on app restart.
Form Draft Persistence
// Auto-save form drafts
function setupFormDraft(formSelector, draftKey) {
// Restore draft on page init
if (app.data[draftKey]) {
$$(formSelector + ' [name]').each(function() {
var name = this.getAttribute('name');
if (app.data[draftKey][name] !== undefined) {
this.value = app.data[draftKey][name];
}
});
}
// Save on input change
$$(formSelector + ' [name]').on('input change', function() {
var formData = {};
$$(formSelector + ' [name]').each(function() {
formData[this.getAttribute('name')] = this.value;
});
app.data[draftKey] = formData;
app.dataSave();
});
// Clear on successful submit
$$(formSelector).on('submit', function() {
delete app.data[draftKey];
app.dataSave();
});
}
// Call for specific forms
setupFormDraft('#contact-form', 'contactDraft');
setupFormDraft('#order-form', 'orderDraft');
Expected output: Form inputs auto-save to localStorage as the user types. If the app closes or refreshes, the form data is restored. Successful submission clears the draft.
Session State
// Session-only data (not persisted)
if (!window.f7Session) {
window.f7Session = {
currentPage: null,
searchQuery: '',
filters: {},
scrollPositions: {}
};
}
// Save scroll position per page
$$(document).on('page:beforeout', '.page', function(e) {
var pageName = e.detail.page.name;
f7Session.scrollPositions[pageName] = window.scrollY;
});
// Restore scroll position
$$(document).on('page:afterin', '.page', function(e) {
var pageName = e.detail.page.name;
var position = f7Session.scrollPositions[pageName];
if (position) {
app.utils.scrollTo(0, position, 0); // Instant scroll
}
});
// Search state across pages
function setSearchQuery(query) {
f7Session.searchQuery = query;
}
function getSearchQuery() {
return f7Session.searchQuery || '';
}
Expected output: Session state preserves scroll positions and search queries during the current session but does not persist across app restarts.
Caching API Data
// Cache API responses
var apiCache = {
get: function(key) {
var cached = localStorage.getItem('apicache-' + key);
if (cached) {
var item = JSON.parse(cached);
if (item.expiry > Date.now()) {
return item.data;
} else {
localStorage.removeItem('apicache-' + key);
}
}
return null;
},
set: function(key, data, ttlMinutes) {
var item = {
data: data,
expiry: Date.now() + (ttlMinutes * 60 * 1000)
};
localStorage.setItem('apicache-' + key, JSON.stringify(item));
},
clear: function() {
Object.keys(localStorage).forEach(function(key) {
if (key.startsWith('apicache-')) {
localStorage.removeItem(key);
}
});
}
};
// Usage in route
{
path: '/products/',
async: function(routeTo, routeFrom, resolve, reject) {
var cached = apiCache.get('products');
if (cached) {
resolve({ template: renderProducts(cached) });
} else {
fetch('/api/products')
.then(function(r) { return r.json(); })
.then(function(data) {
apiCache.set('products', data, 30);
resolve({ template: renderProducts(data) });
});
}
}
}
Expected output: API responses are cached with a TTL. Cached data serves immediately on repeat visits, reducing network requests and improving perceived performance.
User Preferences
// User preferences manager
var Preferences = {
defaults: {
darkMode: false,
fontSize: 16,
language: 'en',
notificationsEnabled: true,
lastVisitedPage: '/',
itemsPerPage: 20
},
load: function() {
var saved = localStorage.getItem('preferences');
var prefs = saved ? JSON.parse(saved) : {};
// Merge with defaults
Object.keys(this.defaults).forEach(function(key) {
if (prefs[key] === undefined) {
prefs[key] = this.defaults[key];
}
}, this);
return prefs;
},
save: function(prefs) {
localStorage.setItem('preferences', JSON.stringify(prefs));
},
get: function(key) {
var prefs = this.load();
return prefs[key] !== undefined ? prefs[key] : this.defaults[key];
},
set: function(key, value) {
var prefs = this.load();
prefs[key] = value;
this.save(prefs);
// Apply immediately
this.apply(key, value);
},
apply: function(key, value) {
switch (key) {
case 'darkMode':
$$('body')[value ? 'addClass' : 'removeClass']('dark-theme');
break;
case 'fontSize':
document.documentElement.style.fontSize = value + 'px';
break;
}
},
applyAll: function() {
var prefs = this.load();
Object.keys(prefs).forEach(function(key) {
this.apply(key, prefs[key]);
}, this);
}
};
// Initialize on app start
app.on('init', function() {
Preferences.applyAll();
});
// Usage
Preferences.set('darkMode', true);
Preferences.set('fontSize', 18);
var lang = Preferences.get('language');
Expected output: User preferences persist across sessions. Setting a preference applies it immediately and saves it to localStorage.
State Restoration
// Restore entire app state on startup
var AppState = {
save: function() {
var state = {
lastPage: app.views.main.router.currentRoute.url,
theme: Preferences.get('darkMode') ? 'dark' : 'light',
storeState: JSON.parse(JSON.stringify(store.state)),
timestamp: Date.now()
};
localStorage.setItem('appstate', JSON.stringify(state));
},
restore: function() {
var saved = localStorage.getItem('appstate');
if (!saved) return false;
try {
var state = JSON.parse(saved);
// Restore store state
Object.assign(store.state, state.storeState);
// Restore theme
if (state.theme === 'dark') {
Preferences.set('darkMode', true);
}
console.log('App state restored from', new Date(state.timestamp));
return state.lastPage;
} catch (e) {
console.error('State restoration failed:', e);
return false;
}
},
clear: function() {
localStorage.removeItem('appstate');
}
};
// Auto-save state periodically
setInterval(function() {
AppState.save();
}, 30000);
// Save on page leave
$$(document).on('page:beforeout', '.page', function() {
AppState.save();
});
// Restore on app init
app.on('init', function() {
var lastPage = AppState.restore();
if (lastPage) {
// Navigate to last visited page
app.views.main.router.navigate(lastPage, { animate: false });
}
});
Expected output: On app restart, the state is restored to the exact point where the user left off β same page, same data, same preferences.
Common Mistakes
Storing large data in app.data - app.data serializes to JSON in localStorage (5-10MB limit). Store only essential state. Cache large data with indexedDB.
Not handling JSON parse errors - localStorage data can be corrupted. Always wrap JSON.parse in try-catch and provide fallback defaults.
Mutating store state directly without actions - Direct mutation bypasses action logic. Always use store.dispatch('actionName') for consistent state changes.
Forgetting to unwatch store watchers - Store watchers persist after page destruction. Call the unwatch function in the page:destroy event.
Storing sensitive data in localStorage - localStorage is not encrypted. Never store passwords, tokens, or sensitive personal data.
Practice Questions
- How do you define a reactive Store with initial state?
- What is the difference between store.state and store.getters?
- How does app.data persist data to localStorage?
- How do you save and restore form drafts?
- How do you cache API responses with expiry?
Challenge: Build a complete state-managed app with: a Store managing todo items with add/remove/toggle actions, getters for completed/pending counts and filtered lists, form draft auto-save for the add-todo form, user preferences (theme, font size, sort order) persisted and restored, session state preserving scroll position per page, API cache for fetching todo suggestions, and full state restoration on app restart.
FAQ
Mini Project
Build a fully state-managed note-taking app with: a Store managing notes (create, edit, delete, archive), reactive UI updates when notes change, form draft auto-save (prevents data loss on accidental back navigation), user preferences (theme, sort order, font size) persisted, session state preserving scroll position and active note, API cache for fetching note templates, and full state restoration that returns the user to the exact note they were editing.
What's Next
State management keeps data organized. Learn how Framework7 Complete Project brings everything together in a full mobile app build.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro