---
title: "Shared and Global State With store()"
description: "store(initial, storageKey?) creates module-scope shared state with the same API as state: exported actions instead of reducers, optional localStorage."
source: https://lemonadejs.com/docs/store/
---

# Shared state with store()

A store is shared state: a state that lives at module scope instead of
inside a component. Same box, same rules: assignment notifies, mutation is free,
`touch()` when you are done:

```javascript
import { store } from 'lemonadejs';

export const session = store({ user: null });       // any component: ${session}
export const theme = store('light', 'app-theme');   // persisted to localStorage
```

Every component that reads `session.value` in a live expression subscribes
that binding; when a component unmounts, its subscriptions are disposed
with it. There is nothing else to learn, because a store *is* a
[State](/docs/state/).

One store at module scope, read by a header and written by buttons: no provider, no context, no dispatch.

<!--example-->

```js
import { html, store } from 'lemonadejs';

const cart = store([]);                                   // module scope
const add = (item) => (cart.value = [...cart.value, item]);
const clear = () => (cart.value = []);

const Header = () => html`<b>${() => cart.value.length} items in the cart</b>`;
const Buy = (props) => html`<button onclick="${() => add(props.item)}">Add ${props.item}</button>`;

const App = () => html`<div>
    <${Header} />
    <p><${Buy} item="Lemon" /> <${Buy} item="Sugar" /> <${Buy} item="Ice" /> <button onclick="${clear}">clear</button></p>
    <ul>${() => cart.value.map((i) => html`<li>${i}</li>`)}</ul>
</div>`;
```

## The problem

Component-scoped state does not cross the component tree without a
dedicated transport. Common patterns (threading props, context,
injectable services) each introduce a separate state model beside the
component one, with its own update vocabulary and a boundary the author
crosses on every interaction.

## How LemonadeJS solves it

`store()` is the component `state` tool minus the component. One reactive
primitive, two scopes:

```javascript
// store.js: state and the actions that own it, one module
import { store } from 'lemonadejs';

export const cart = store([]);

export const add = (item) => {
    cart.value = [...cart.value, item];     // assignment notifies
};

export const clear = () => {
    cart.value = [];
};
```

```javascript
// anywhere: no provider, no wrapper, no dispatch
import { cart, add } from './store.js';

const Header = () => html`<span>${() => cart.value.length} items</span>`;
const Buy = (props) => html`<button onclick="${() => add(props.item)}">Add</button>`;
```

This module shape (a store plus exported action functions) is the
deliberate replacement for the action/reducer/dispatch pipeline (and for
v5's string-keyed Sugar `set`/`get`/`dispatch`). The actions are plain
functions; "dispatching" is calling them; the import list is the complete
inventory of who can write.

What carries over from the State contract, because it is the same class:

- **Updates reach exactly the dependent bindings.** A store written from
  one component updates only the expressions that read it, in any other
  component anywhere in the page (pinned in the engine's suite), and it
  stays live across unmount/remount cycles of its consumers.
- **Big data stays cheap**: mutate in place, `touch()` once, `batch()` for
  bulk passes.
- **`subscribe(cb)` is untracked** (addEventListener semantics) and
  returns its unsubscribe function; `peek()` reads without subscribing:
  the same two calls that adapt a store to React via
  `useSyncExternalStore(s.subscribe, s.peek)`.
- **Destroy accounting holds**: the leak gate asserts that a shared
  store's subscription set returns to zero after 200 mount/unmount cycles
  of its consumers; see [Destroy](/docs/destroy/). The one caller-owned
  case is a manual `subscribe()` outside a component: you hold the
  unsubscribe function.

**Persistence is the second argument.** `store(initial, 'key')` reads the
key from `localStorage` at creation (falling back to `initial` if the
entry is missing or unparseable) and writes JSON back on every
notification: assignments *and* `touch()`. Storage failures (quota,
privacy mode, no `localStorage` at all) degrade silently to in-memory:

```javascript
const a = store({ count: 1 }, 'test-key');
a.value = { count: 7 };                  // localStorage now holds {"count":7}
const b = store({ count: 0 }, 'test-key');
b.value;                                 // { count: 7 }, restored on creation
```

That example is the engine's own test, verbatim.

**No action history or replay.** v6 stores are mutable, so there is no
action log or time-travel replay. `inspect()` returns the live component
tree with state values as JSON; synchronous updates make stepping in a
debugger straightforward.

**SSR.** `store()` at module scope is shared per JavaScript module
instance. In an SSR process that means shared across requests;
server-rendered apps must scope stores per request themselves.

## In practice

The Studio blocks consume stores through their props, the same two-way
`bind` that takes a local state takes a store, which is how one value is
shared between a block and the rest of the page:

```javascript
// the dropdown demo: one store, bound to the component AND rendered beside it
const team = store('');

html`<${Dropdown} data="${countries}" bind="${team}" placeholder="Pick a country" />
     <p>bound value: <b>${team}</b></p>`;
// picking an option updates the store; writing team.value = 'br'
// updates the dropdown, silently (no onchange echo)
```

```javascript
// the calendar demo: a persisted date would be ONE argument away
const date = store('2026-06-15');                 // store('2026-06-15', 'last-date')
html`<${Calendar} bind="${date}" format="DD/MM/YYYY" />`;
```

The block test suites use the same gesture the other way around, binding
a store, writing it externally, and asserting the component followed
(switch, progress, wheel, transferlist), so "a store is a state
everywhere a state is accepted" is a tested property, not a convention.

## Reference

```javascript
import { store } from 'lemonadejs';

const s = store(initial);             // State<T> at module scope
const p = store(initial, 'key');      // + localStorage: restore on creation,
                                      //   persist on every notification

// the full State API applies:
s.value                               // read (subscribes inside live expressions)
s.value = next                        // assign: notifies, synchronously
s.touch()                             // notify after in-place mutation (also persists)
s.peek()                              // read without subscribing
s.subscribe(cb)                       // untracked listener → unsubscribe fn
batch(fn)                             // dedupe notifications across stores and states
```

Rules worth memorizing: a store outlives every component, so bindings from
components are disposed with them, manual `subscribe()` is yours to
unsubscribe; persistence is JSON (`Date`, `Map`, functions will not
round-trip); `computed()` is a component tool, so derive from stores at use
sites. For component-owned *services* rather than shared *data*, see
[Sugar](/docs/sugar/).