API
nsh/storage
The storage API keeps the app’s data on the device: strings by string key, still there the next time the app starts. Use it for settings, drafts, the last tab the user had open, a cache of what the server sent — any state that should outlive the app’s process. Rust code uses nsh_core::storage::Storage, TSX imports Storage from nsh/storage; both read and write the same entries, so an app with a Rust entry and a TSX entry sees one set of data.
// Storage: a launch counter kept across reloads, the stored keys and a reset.
import { mount, signal, Text, View } from "nsh";
import { Button } from "nsh/base";
import { Storage } from "nsh/storage";
import { size, ui, wide } from "./palette";
function App() {
const opened = signal<number | null>(null);
const keys = signal<string[]>([]);
const start = async () => {
const count = Number((await Storage.getItem("opened")) ?? "0") + 1;
await Storage.setItem("opened", String(count));
await Storage.setItem("lastOpened", new Date().toISOString());
opened.set(count);
keys.set(await Storage.keys());
};
const reset = async () => {
await Storage.clear();
opened.set(0);
keys.set(await Storage.keys());
};
start();
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: wide() ? 20 : 16, paddingTop: 24, gap: 12, alignItems: wide() ? "flex-start" : "stretch" }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>Storage</Text>
<View style={{ borderRadius: 14, padding: 16, gap: 6, ...ui().card, width: wide() ? 440 : undefined }}>
<Text style={{ fontSize: 28, fontWeight: "700", color: ui().text }}>
{opened() === null ? "Reading…" : `Opened ${opened()} times`}
</Text>
<Text style={{ color: ui().muted }}>Reload the page: the count is still there.</Text>
</View>
<Text style={{ color: ui().muted }}>{`Keys: ${keys().length ? keys().join(", ") : "none"}`}</Text>
<Button
onPress={reset}
style={wide() ? { ...ui().raised, height: size.control } : ui().raised}
class={wide() ? "justify-center rounded-lg px-3 active:opacity-75" : "items-center rounded-xl py-3 active:opacity-75"}
>
<Text style={{ fontWeight: "600", fontSize: wide() ? 13 : undefined, color: ui().text }}>Reset</Text>
</Button>
</View>
);
}
mount(App);use nsh_core::storage::Storage;import { Storage } from "nsh/storage";const Storage: {
getItem(key: string): Promise<string | null>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
keys(): Promise<string[]>;
clear(): Promise<void>;
};
| TS | Rust (nsh_core::storage::Storage) |
|---|---|
await Storage.getItem(key) | Storage::get_item(key) -> Option<String> |
await Storage.setItem(key, value) | Storage::set_item(key, value) -> Result<(), StorageFull> |
await Storage.removeItem(key) | Storage::remove_item(key) |
await Storage.keys() | Storage::get_all_keys() -> Vec<String> |
await Storage.clear() | Storage::clear() |
The Rust functions are synchronous, as the entries are in memory: call them where components run (their bodies, handlers and tasks).
Each app has a storage of its own, up to 5 MB of keys and values. Calls run in the order the app makes them, so a getItem after a setItem sees the new value without waiting for the write. Reads come from memory; writes reach the disk in the background, a few at a time.
| Platform | Where the data is kept |
|---|---|
| iOS | Library/Application Support/nsh/<app id>/storage.json in the app’s container |
| Android | files/nsh/<app id>/storage.json in the app’s private directory |
| macOS | ~/Library/Application Support/<app id>/storage.json |
| Windows | %APPDATA%\<app id>\storage.json |
| Linux | $XDG_DATA_HOME/<app id>/storage.json (~/.local/share/…) |
| Web | The browser’s localStorage, under keys named after the app id (nsh:com.example.todo:…) |
A storage.json that is damaged is kept as storage.json.corrupt next to it, and the app starts with an empty storage; one that cannot be read leaves the app’s changes in memory, so the file is never replaced.
The app id is the id in nsh.json, and it travels with the app: nsh build and nsh package write it next to the bundle, the dev server sends it, and a signed package carries the one its release key’s certificate vouches for. So every app keeps its own data with no setting, wherever it runs; an app without an id keeps it under default. An embedding app can choose another place (see Embedding on iOS and Embedding on Android).
#Storage.getItem
getItem(key: string): Promise<string | null>;
Resolves with the value of key, or null when it has none. Rust: Storage::get_item(key), None when it has none.
let theme = Storage::get_item("theme").unwrap_or_else(|| "system".into());const theme = (await Storage.getItem("theme")) ?? "system";#Storage.setItem
setItem(key: string, value: string): Promise<void>;
Sets key to value, replacing what it held. Rejects when the storage would grow past 5 MB; the value is not kept then. Rust: Storage::set_item(key, value), which returns Err(StorageFull) then. Values are strings: keep structured data as JSON (serde_json::to_string in Rust, JSON.stringify in TSX) and read it back (serde_json::from_str, JSON.parse).
let draft = serde_json::json!({ "title": title.get(), "body": body.get() });
if let Err(full) = Storage::set_item("draft", draft.to_string()) {
log::warn!("{full}");
}Storage.setItem("draft", JSON.stringify({ title: title(), body: body() }));#Storage.removeItem
removeItem(key: string): Promise<void>;
Removes key and its value. Removing a key that has no value does nothing. Rust: Storage::remove_item(key).
#Storage.keys
keys(): Promise<string[]>;
Resolves with every key, in order. Rust: Storage::get_all_keys().
#Storage.clear
clear(): Promise<void>;
Removes every key of the app, such as when the user signs out. Rust: Storage::clear().
#Keeping a signal on the device
Read the stored value when the app starts, then write it back whenever the signal changes:
use nsh_core::{storage::Storage, ui::prelude::*};
#[component]
pub fn App() -> impl IntoElement {
let saved = Storage::get_item("count").and_then(|n| n.parse().ok());
let count = signal(saved.unwrap_or(0_u32));
effect(move || {
if let Err(full) = Storage::set_item("count", count.get().to_string()) {
log::warn!("{full}");
}
});
view()
.on_press(move || count.update(|n| *n += 1))
.child(text(move || format!("Pressed {} times", count.get())))
}import { effect, mount, signal, Text, View } from "nsh";
import { Storage } from "nsh/storage";
const count = signal(0);
const loaded = signal(false);
Storage.getItem("count").then((saved) => {
count.set(Number(saved ?? 0));
loaded.set(true);
});
effect(() => {
if (loaded()) Storage.setItem("count", String(count()));
});
mount(() => (
<View onPress={() => count.update((n) => n + 1)}>
<Text>Pressed {count()} times</Text>
</View>
));