Storage
localStorage keeps small values on the device — a setting, the progress, a draft — as strings
under keys, still there when the app opens again. Small values that must survive a restart go
here or into a stored signal (signal(x).stored(key), signals); documents
and binary files go to files (desktop and iOS); data shared between devices or
users goes to the backend database (server code).
- remember a setting between launches (
signal(x).stored(key)) - save game progress or a high score on the device
- show the onboarding only on the first launch
- keep a list or an object between launches (as JSON)
- forget a saved value or reset the progress (
removeItem)
At a glance
type Save = { level: number, coins: number }
// save: a string under a key, still there the next time the app opens
localStorage.setItem('save', JSON.stringify({ level: 3, coins: 120 }))
// load: null when nothing was saved (the first launch), so fall back to a default
const raw = localStorage.getItem('save')
const save: Save = raw ? JSON.parse(raw) : { level: 1, coins: 0 }
console.log('level', save.level, 'coins', save.coins)
// forget
localStorage.removeItem('save')Save progress or a high score: setItem / getItem / removeItem
The high score, the level reached or any value kept for the next launch is a string under a key:
setItem writes it, getItem reads it back (null while nothing is saved under the key — the
first launch), removeItem forgets it.
localStorage.setItem(key: string, value: string): void // store or overwrite
localStorage.getItem(key: string): string | null // null = nothing saved under the key
localStorage.removeItem(key: string): void // delete; a key that is not there is fineThat is the whole surface — no clear(), no length, no key(i): to reset everything, remove
the keys you wrote.
let best = Number(localStorage.getItem('best') ?? 0) // 0 on the first launch
const gameOver = (score: number) => {
if (score <= best) return
best = score
localStorage.setItem('best', String(best)) // values are strings: String() a number
}
const resetProgress = () => {
best = 0
localStorage.removeItem('best') // getItem('best') is null again
}
gameOver(120)
gameOver(80)
console.log('best', localStorage.getItem('best')) // "120"
resetProgress()
console.log('after reset', localStorage.getItem('best')) // nullvalues are strings only — String(n) a number and Number(text) it back. A
non-string passed past the types is stored as its string form (5 reads back as "5").
Keep a list or an object: JSON
A list or an object is kept as JSON text under one key: JSON.stringify on the way in,
JSON.parse on the way out, and the whole list written back after each change. A stored signal
does this for you (below).
type Note = { id: number, text: string }
const loadNotes = (): Note[] => {
try { return JSON.parse(localStorage.getItem('notes') ?? '[]') }
catch { return [] } // not JSON under the key: start empty
}
const saveNotes = (list: Note[]) => localStorage.setItem('notes', JSON.stringify(list))
const notes = loadNotes()
notes.push({ id: Date.now(), text: 'Buy milk' })
saveNotes(notes) // the whole list goes back under the key
console.log('notes', loadNotes().length, loadNotes()[0].text)Remember a setting between launches: signal(x).stored(key)
A setting that must still be there after the app is closed and opened again — sound on, the theme,
the last tab — is a stored signal: signal(initial).stored(key) starts from what the last run
saved under key (the initial value on the first launch), and every write is saved at once. More
in signals.
const soundOn = signal(true).stored('soundOn') // what the last run saved; true on the first launch
UIScreen(
UIButton(UIText(() => (soundOn.value ? 'Sound: on' : 'Sound: off')).style({ color: 'white' }))
.style({ bgColor: '#FF4032', borderRadius: 12, p: 16 })
.onClick(() => { soundOn.value = !soundOn.value }), // saved as it is written
).style({ bgColor: 'black', p: 20 }).open()- It is
localStorageunderneath: the value is saved as JSON under the same key (localStorage.getItem('soundOn')reads"false"), so give each signal a key of its own. - JSON values only: a
Dateor a class instance does not come back as one. - Write a new array or object (
list.value = [ ...list.value, item ]): one changed in place is the same value — nothing is told and nothing is saved.
Show onboarding only on the first launch
A flag under a key tells the first launch from the others: getItem returns null until the flag
is set, so open the onboarding then, and set the flag when the user is through it.
const home = UIScreen(
UIText('Home').style({ fontSize: 24, color: 'white' }),
).style({ bgColor: 'black', p: 20 })
const welcome = UIScreen(
UIText('Welcome!').style({ fontSize: 24, color: 'white' }),
UIButton(UIText('Get started').style({ color: 'white' }))
.style({ bgColor: '#FF4032', borderRadius: 12, p: 16 })
.onClick(() => {
localStorage.setItem('onboarded', '1') // from now on the app opens on home
Router.replace(home)
}),
).style({ bgColor: 'black', p: 20, gap: 16 })
Router.init(localStorage.getItem('onboarded') ? home : welcome) // null = the first launchWhen it is written
Reads and writes are synchronous: getItem right after setItem returns the new value. The device
keeps the whole store as one document, rewritten after a change — at the next frame (on the web,
as soon as the running code finishes) — so a burst of setItem calls is one write. Keep it small:
each save writes all of it, and on the web a store over the browser's quota is not saved, with no
error. A document or a long list belongs in files or the
database.
Save a value when it changes, not in a pause handler (app lifecycle): on iOS and
Android the frames stop right after pause, so a value set there is written only when the app
comes back — and is lost if the OS closes the app in the background.
Where it is kept: a plain file, not for secrets
The store is a plain JSON file — in the app's own folder (its sandbox) on iOS and Android, under
the user's data folder on desktop, in the browser's localStorage on the web — with no secure
storage (Keychain / Keystore) for passwords, tokens or keys. Nothing syncs it: another device or
another user does not see what this one keeps.
a test run (lecodes render / lecodes test, headless or on the desktop host) keeps
the store in memory for that run only — every run starts empty.
projects opened in the le.codes viewer app from a link or a QR code share one store on that device — a key another project wrote can already be there. An app built on its own has its own store.
Pitfalls
localStorage['theme'] = 'dark' // the browser habit works: the same store as setItem
localStorage.theme // 'dark' — undefined for a key never set (getItem gives null)
localStorage['count'] = 5 // stored as the string "5", as in the browserSee also
- Signals —
signal(x).stored(key)and the rest of the reactive state - Files & share —
filesfor documents and binary files (desktop, iOS) - Database — data shared between devices and users (server code)
- App lifecycle — the
pause/resumeevents