Central Store with Elemento
This guide shows how to create a tiny, framework-free “central store” using Elemento’s signals. It’s based on the live example in src/__tests__/store.html and src/__tests__/store.js.
You’ll learn how to:
- Model app state with signals
- Expose read/write APIs (fetch/update)
- Consume the store from Web Components built with Elemento
Why a central store?
- Share state across multiple components
- Keep async data fetching in one place
- Provide a single source of truth with a small, predictable API
Elemento uses signals powered by Preact Signals Core under the hood, so any component reading from the store will re-render automatically when the store changes.
Quick start: A minimal Todos store (JavaScript)
store.js
import { signal } from '@solidx/elemento';
// Or, if you run from this repo without bundler:
// import { signal } from 'Elemento';
import { getTodos, addTodo as add } from './TodoService.js';
// 1) Define reactive state
const todos = signal([]);
// 2) Export read-only handles (by convention)
export const store = { todos };
// 3) Write-side API (async-safe)
export async function fetchTodos() {
const t = await getTodos();
// Always assign a new array so change is detected
todos.value = [...t];
}
export async function addTodo(todo) {
const t = await add(todo);
todos.value = [...t];
}
A tiny mock service for this example:
TodoService.js
let data = { todos: [{ title: 'Clean the kitchen' }] };
export function getTodos() {
return new Promise(resolve => {
setTimeout(() => resolve(data.todos), 500);
});
}
export function addTodo(todo) {
return new Promise(resolve => {
setTimeout(() => {
data.todos = [...data.todos, todo];
resolve(data.todos);
}, 300);
});
}
Consuming the store from a component
- Read store signals with
computed(or directly via.valueinside the template function). - The component re-renders whenever any used signal changes.
import { Elemento, computed, html } from '@solidx/elemento';
import { store } from './store.js';
function TodoList() {
// derive reactive value from the store
const todos = computed(() => store.todos.value);
return html`
<section>
<h3>Todos</h3>
${todos.value?.map(t => html`<p>${t.title}</p>`)}
</section>`;
}
// Elemento accepts an options object; none are needed here.
customElements.define('todo-list', Elemento(TodoList));
You can keep composing components like usual:
function TodoApp() {
return html`
<todo-list></todo-list>
<button @click=${() => import('./store.js').then(m => m.addTodo({ title: 'New todo' }))}>
Add todo
</button>
`
}
customElements.define('todo-app', Elemento(TodoApp));
Using in the browser (no bundler)
Use an import map to point Elemento to the built file under dist/ (this repo’s test pages do this):
<script type="importmap">
{
"imports": {
"Elemento": "/dist/index.js"
}
}
</script>
<script type="module">
import { Elemento, computed, html } from 'Elemento';
import { store, fetchTodos } from './store.js';
fetchTodos();
// Define and use components that read from store...
</script>
Using with a bundler
Install the package:
npm install @solidx/elemento
Then import from the package name in your app code:
import { signal, computed, Elemento, html } from '@solidx/elemento';
TypeScript tip: you can type the options object if you pass one:
import type { ElementoOptions } from '@solidx/elemento';
const opts: ElementoOptions<never, never> = {};
customElements.define('todo-list', Elemento(TodoList, opts));
Patterns and best practices
- Immutable updates: avoid mutating arrays/objects in-place. Reassign with new references so signals propagate changes. For example:
- Good:
todos.value = [...todos.value, newTodo] - Avoid:
todos.value.push(newTodo)
- Good:
-
Granular signals: keep separate signals for independent concerns (e.g.,
todos,loading,error) so components update only when needed. - Derived selectors: compute projections once and share them between components. ```js import { computed } from ‘@solidx/elemento’; import { store } from ‘./store.js’;
export const completedTodos = computed(() => store.todos.value.filter(t => t.completed) );
- Write helpers: keep all writes in the store module to centralize side effects.
```js
export async function toggleTodo(id) {
const next = store.todos.value.map(t => t.id === id ? { ...t, completed: !t.completed } : t);
store.todos.value = next;
}
- Loading and error states: ```js const loading = signal(false); const error = signal(null); export const store = { todos, loading, error };
export async function fetchTodos() { loading.value = true; error.value = null; try { const t = await getTodos(); todos.value = […t]; } catch (e) { error.value = e; } finally { loading.value = false; } }
---
## Full inline example (like `src/__tests__/store.html`)
```html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<script type="importmap">
{
"imports": {
"Elemento": "../../dist/index.js"
}
}
</script>
</head>
<body>
<script type="module">
import { Elemento, computed, html } from 'Elemento';
import { store, fetchTodos, addTodo } from './store.js';
function TodoList() {
// derive reactive value from the store
const todos = computed(() => store.todos.value);
return html`
<section>
<h3>Todos</h3>
${todos.value?.map(t => html`<p>${t.title}</p>`)}
</section>`;
}
customElements.define('todo-list', Elemento(TodoList));
function TodoApp() {
return html`
<todo-list></todo-list>
<button @click=${() => addTodo({ title: 'New todo' })}>add todo</button>
`;
}
customElements.define('my-data-button', Elemento(TodoApp));
fetchTodos();
</script>
<my-data-button></my-data-button>
</body>
</html>
That’s it! You now have a lightweight, reactive central store you can consume from any Elemento component.