Skip to content

Store

Store is JOPS’s app-wide data store: one shared key-value object with dot-path access. It is deliberately simple. There are no subscriptions and no reactivity: writing to the Store does not update any view. Views read from it when they come to the front, and write to it in event handlers.

import { Store } from "jops-core";
const store = Store.singleton(); // the single instance, created on first call
store.set("user.name", "Alice"); // creates intermediate objects
store.get("user.name"); // "Alice"
store.get("user"); // { name: "Alice" }
store.get("missing.path"); // undefined
store.set("cart.items", [1, 2, 3]);
store.clear(); // resets everything to {}

Paths use dots: "user.name" reads name inside user. set() creates the objects along the way.

Because the Store is not reactive, read it at the moment a view is shown. onResume() runs on first display and every time the view comes back to the front, so the view always shows current data:

src/Profile.js
import { View, Store, layout } from "jops-core";
export default class Profile extends View {
onResume() {
const user = Store.singleton().get("user");
this.get("name").set(layout`${user?.name ?? "Guest"}`);
}
}

Rather than scattering Store paths across views, put the reads and writes for one kind of data in a small model object. To let visible views update right away, have it announce every change on the EventBus:

src/model/tasks.js
import { Store, EventBus } from "jops-core";
export const Tasks = {
all: () => Store.singleton().get("tasks") ?? [],
save(list) {
Store.singleton().set("tasks", list);
EventBus.send("tasks:changed");
},
add(title) {
Tasks.save([...Tasks.all(), { id: Date.now(), title, done: false }]);
},
remove(id) {
Tasks.save(Tasks.all().filter((t) => t.id !== id));
},
};

Views read with Tasks.all() and subscribe to "tasks:changed" in onLayout().

A list screen can keep what it fetched in the Store, so a detail screen opened from it can show a record without another request:

src/Quotes.js
async onLayout() {
const quotes = await fetch("/api/quotes").then((r) => r.json());
Store.singleton().set("quotes", quotes);
this.showRows(quotes);
}
src/QuoteDetails.js
async onResume() {
const { id } = Router.singleton().getParams();
const cached = (Store.singleton().get("quotes") ?? []).find((q) => q.id === id);
// fall back to the API on a direct link or a reload
const quote = cached ?? (await fetch(`/api/quotes/${id}`).then((r) => r.json()));
this.showQuote(quote);
}

When a write happens on another screen, the list can be told to reload without being called directly. The screen that saves sets a flag; the list checks it in onResume():

// after a successful save, in the editor
Store.singleton().set("quotesStale", true);
// in the list view
async onResume() {
if (Store.singleton().get("quotesStale")) {
Store.singleton().set("quotesStale", false);
await this.reload();
}
}