Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
04af902
feat: init commits for marko-store
defunkt-dev Jun 26, 2026
6ac2983
fix type issues
defunkt-dev Jun 26, 2026
c93c86c
knip failure fix
defunkt-dev Jun 26, 2026
3d003fd
e2e success with knip fails
defunkt-dev Jun 26, 2026
639239f
working version with e2e and knip working
defunkt-dev Jun 26, 2026
dc45a9a
some fixes + more tests
defunkt-dev Jun 27, 2026
db6f10d
updte gitignore
defunkt-dev Jun 27, 2026
204ad26
feat: 2 more tags + ssr per request + some fixes
defunkt-dev Jun 28, 2026
8dce8b5
multi-store cases
defunkt-dev Jun 28, 2026
acdb767
Fix more things
defunkt-dev Jun 28, 2026
6f28126
update README
defunkt-dev Jun 28, 2026
a1f4789
update README
defunkt-dev Jun 28, 2026
5a19ecb
more tests + readme updates
defunkt-dev Jun 28, 2026
9bc3dd7
more tests + readme updates
defunkt-dev Jun 28, 2026
2f3d814
readme updates
defunkt-dev Jun 28, 2026
7d8953d
readme updates
defunkt-dev Jun 28, 2026
65f3051
readme updates
defunkt-dev Jun 28, 2026
7f7e598
readme updates
defunkt-dev Jun 28, 2026
d6c8826
more tests + readme
defunkt-dev Jun 29, 2026
9f56a8d
add tests for <await> usage and out of order render
defunkt-dev Jun 29, 2026
3844bac
streaming-store-provider
defunkt-dev Jun 30, 2026
470197f
+ more test
defunkt-dev Jun 30, 2026
7045642
ignore a generated results.json
defunkt-dev Jun 30, 2026
7be7b11
update marko examples
defunkt-dev Jun 30, 2026
c19cbca
update examples with lan tools as typecheck fails and add .d files
defunkt-dev Jun 30, 2026
c880a77
update docs/
defunkt-dev Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-marko-store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/marko-store": minor
---

Add `@tanstack/marko-store`, a Marko 6 adapter for TanStack Store. It provides the `<store-selector>` and `<store-atom>` tags — the Marko equivalents of the `useSelector` / `useStore` adapters — and re-exports the full `@tanstack/store` core.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ examples/angular/store-context/.angular/cache/21.2.7/store-context/angular-compi
examples/angular/stores/.angular/cache/21.2.7/stores/.tsbuildinfo
examples/angular/stores/.angular/cache/21.2.7/stores/angular-compiler.db
examples/angular/stores/.angular/cache/21.2.7/stores/angular-compiler.db-lock
test-results/
*.tsbuildinfo
packages/marko-store/e2e/pw-results.json
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"plugins": ["prettier-plugin-svelte"],
"plugins": ["prettier-plugin-svelte", "prettier-plugin-marko"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
35 changes: 35 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@
"to": "framework/lit/quick-start"
}
]
},
{
"label": "marko",
"children": [
{
"label": "Quick Start - Marko",
"to": "framework/marko/quick-start"
}
]
}
]
},
Expand Down Expand Up @@ -159,6 +168,12 @@
"to": "framework/lit/reference/index"
}
]
},
{
"label": "marko",
"children": [
{ "label": "Marko Tags", "to": "framework/marko/reference/index" }
]
}
]
},
Expand Down Expand Up @@ -507,6 +522,16 @@
"to": "framework/lit/reference/interfaces/UseSelectorOptions"
}
]
},
{
"label": "marko",
"children": [
{ "label": "<store-provider>", "to": "framework/marko/reference/tags/store-provider" },
{ "label": "<store-context>", "to": "framework/marko/reference/tags/store-context" },
{ "label": "<store-selector>", "to": "framework/marko/reference/tags/store-selector" },
{ "label": "<store-atom>", "to": "framework/marko/reference/tags/store-atom" },
{ "label": "<stream-store-provider>", "to": "framework/marko/reference/tags/stream-store-provider" }
]
}
]
},
Expand Down Expand Up @@ -672,6 +697,16 @@
"to": "framework/lit/examples/simple"
}
]
},
{
"label": "marko",
"children": [
{ "label": "Simple", "to": "framework/marko/examples/simple" },
{ "label": "Atoms", "to": "framework/marko/examples/atoms" },
{ "label": "Stores", "to": "framework/marko/examples/stores" },
{ "label": "Store Actions", "to": "framework/marko/examples/store-actions" },
{ "label": "Store Context", "to": "framework/marko/examples/store-context" }
]
}
]
}
Expand Down
88 changes: 88 additions & 0 deletions docs/framework/marko/quick-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
title: Quick Start
id: quick-start
---

The basic Marko app example to get started with TanStack `marko-store`.

Marko exposes the store through tags rather than hooks. `<store-selector>` reads a slice of a store and keeps it live, re-rendering only when that selected slice changes. State is updated with `store.setState`, exactly as in the other adapters.

```marko
import { createStore } from '@tanstack/marko-store'

// You can instantiate a Store outside of Marko templates too!
export const store = createStore({
dogs: 0,
cats: 0,
})

<div>
<h1>How many of your friends like cats or dogs?</h1>
<p>
Press one of the buttons to add a counter of how many of your friends
like cats or dogs.
</p>

<button onClick() { store.setState((s) => ({ ...s, dogs: s.dogs + 1 })) }>
My Friend Likes dogs
</button>
// <store-selector> subscribes only to state.dogs
<store-selector/dogs from=() => store selector=(state) => state.dogs/>
<div>dogs: ${dogs}</div>

<button onClick() { store.setState((s) => ({ ...s, cats: s.cats + 1 })) }>
My Friend Likes cats
</button>
<store-selector/cats from=() => store selector=(state) => state.cats/>
<div>cats: ${cats}</div>
</div>
```

`<store-selector>` binds the latest selected value to its tag variable (here `dogs` and `cats`) and only updates when that specific selection changes.

## A writable value: `<store-atom>`

For a single writable value, `<store-atom>` gives two-way access to an atom — read its tag variable, and assign to it to write back:

```marko
import { createAtom } from '@tanstack/marko-store'
export const countAtom = createAtom(0)

<store-atom/count from=() => countAtom/>
<button onClick() { count++ }>count is ${count}</button>
```

## Sharing stores without imports: `<store-provider>` and `<store-context>`

To distribute one or more stores down the tree without importing them everywhere, wrap a subtree in `<store-provider>` with a bundle of stores, read any value below with `<store-selector>` in context mode, and grab the bundle to write with `<store-context>`:

```marko
import { createStore } from '@tanstack/marko-store'

<store-provider value=() => ({ cats: createStore({ count: 0 }) })>
<store-selector/catCount context=(c) => c.cats selector=(s) => s.count/>
<div>cats: ${catCount}</div>

<store-context/ctx/>
<button onClick() { ctx().cats.setState((s) => ({ count: s.count + 1 })) }>add a cat</button>
</store-provider>
```

## Server rendering and streaming

`marko-store` is resumable: a store rendered on the server stays live on the client after resume, with no re-execution. For server-computed, per-request data that arrives mid-render through `<await>`, create the store from the awaited value inside the await using `<stream-store-provider>`. The value then renders on the server with no first-paint flash and the store stays live after resume:

```marko
import { createStore } from '@tanstack/marko-store'

<await|user|=fetchUser()>
<stream-store-provider key='user' value=() => ({ user: createStore(user) })>
<store-selector/name key='user' context=(c) => c.user selector=(s) => s.name/>
<div>${name}</div>
</stream-store-provider>
</await>
```

Use a plain `<await>` for in-order streaming, or wrap it in `<try>` with a `<@placeholder>` for out-of-order streaming. Each `<stream-store-provider>` needs a distinct `key`.

For production SSR, use [`@marko/run`](https://github.com/marko-js/run), Marko's official server framework. Standalone `@marko/vite` SSR is intended for development only.
20 changes: 20 additions & 0 deletions docs/framework/marko/reference/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
id: "@tanstack/marko-store"
title: "@tanstack/marko-store"
---

# @tanstack/marko-store

The Marko adapter exposes its API as custom tags. Each tag is documented on its own page below.

## Tags

- [`<store-provider>`](tags/store-provider.md)
- [`<store-context>`](tags/store-context.md)
- [`<store-selector>`](tags/store-selector.md)
- [`<store-atom>`](tags/store-atom.md)
- [`<stream-store-provider>`](tags/stream-store-provider.md)

## Re-exports

`@tanstack/marko-store` re-exports the full `@tanstack/store` core, including `createStore`, `createAtom`, `Store`, `Derived`, and `shallow`. These are framework-agnostic; see the Store API Reference for their documentation.
29 changes: 29 additions & 0 deletions docs/framework/marko/reference/tags/store-atom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
id: store-atom
title: "<store-atom>"
---

# Tag: `<store-atom>`

Binds a single writable atom from `createAtom()` as a two-way value: it reads the atom's current value and writes back through the atom's `set`. Use it for a standalone writable value; for a `Store` (from `createStore`), read it with `<store-selector>` instead.

```marko
<store-atom/qty from=() => qtyAtom>
<p>Quantity: ${qty}</p>
```

## Type parameters

- **`T`** — the atom's value type.

## Attributes

- **`from`** — `() => { get(); set(); subscribe() }` (required). A thunk returning the atom (from `createAtom()`). The atom must have a `set` method; a `Store` does not and will throw.

## Tag variable

The atom's current value, typed `T`. The tag exposes a `value`/`valueChange` pair, so it is two-way bindable — bind it to a variable with `value:=` and assignments write back through the atom's `set`.

## Behavior

The value is seeded from `from().get()` at render and kept live by a subscription on mount, so it updates whenever the atom changes from anywhere. Writes go through `from().set(next)`. If `from()` returns something without a `set` method (such as a `Store`), the tag throws with guidance to use `<store-selector>`.
28 changes: 28 additions & 0 deletions docs/framework/marko/reference/tags/store-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
id: store-context
title: "<store-context>"
---

# Tag: `<store-context>`

Reads the store bundle provided by the nearest `<store-provider>` with a matching key, so a descendant can use the live stores directly (for example to dispatch actions). Binds a getter.

```marko
<store-context/getBundle>
<const/cart=getBundle().cart>
<button onClick() { cart.setState((s) => ({ count: s.count + 1 })) }>
Add one
</button>
```

## Attributes

- **`key`** — `string` (optional, default `"__tanstack_store_context"`). The context key to read. Must match the key the `<store-provider>` used.

## Tag variable

A getter function, `() => bundle`. Call it to read the current bundle parked by the provider. It is a thunk rather than the bundle itself so the read happens at call time, after the provider has parked the bundle.

## Behavior

At render the tag checks that a bundle exists on `$global` under the key and throws if none is found — that is, when there is no `<store-provider>` above it. It does not subscribe to anything; use `<store-selector>` when you need a value that re-renders on change.
28 changes: 28 additions & 0 deletions docs/framework/marko/reference/tags/store-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
id: store-provider
title: "<store-provider>"
---

# Tag: `<store-provider>`

Creates a bundle of stores for the current request and makes it available to descendant tags by a context key. Wraps the content it is given.

```marko
<store-provider value=() => ({ cart: createStore({ items: [] }) })>
<cart-view/>
</store-provider>
```

## Attributes

- **`value`** — `() => Record<string, unknown>` (required). A thunk returning the bundle of named stores or atoms to provide. It is called once on the server at render and again on the client at mount, so each environment gets its own live instances.
- **`key`** — `string` (optional, default `"__tanstack_store_context"`). The context key the bundle is parked under. Use distinct keys to provide more than one bundle on a page. The matching `<store-context>` and context-mode `<store-selector>` must use the same key.
- **`content`** — `Marko.Body` (optional). The body rendered inside the provider, usually supplied implicitly as the tag's children.

## Tag variable

None. `<store-provider>` is a wrapper: it renders its content and binds no tag variable.

## Behavior

The bundle is parked on `$global` under the context key at render, and re-created and published on mount so client subscribers attach to live instances. Only one provider may claim a given key: a second `<store-provider>` (or a `<stream-store-provider>`) on the same key throws, to prevent two writers silently clobbering the same box. On destroy the box and its owner marker are cleared.
38 changes: 38 additions & 0 deletions docs/framework/marko/reference/tags/store-selector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
id: store-selector
title: "<store-selector>"
---

# Tag: `<store-selector>`

Subscribes to a store or atom, projects a slice of it, and re-renders when that slice changes. Works in two modes: `from` (a store passed directly) or `context` (a member of the provided bundle). Binds the selected value.

```marko
<!-- from mode -->
<store-selector/count from=() => cartStore selector=(s) => s.items.length>
<p>${count} items</p>

<!-- context mode -->
<store-selector/count context=(b) => b.cart selector=(s) => s.items.length>
```

## Type parameters

- **`TSource`** — the snapshot type of the source store or atom.
- **`TSelected`** — the projected slice type. Defaults to `TSource` when no `selector` is given.

## Attributes

- **`from`** — `() => { get(); subscribe() }` (optional). A thunk returning the source store or atom directly. Mutually exclusive with `context`.
- **`context`** — `(bundle) => { get(); subscribe() }` (optional). Selects the source from the provided bundle. Mutually exclusive with `from`. Annotate the parameter in typed projects, e.g. `context=(b: any) => b.cart`.
- **`key`** — `string` (optional, default `"__tanstack_store_context"`). The context key to read in `context` mode. Must match the provider's key.
- **`selector`** — `(snapshot: TSource) => TSelected` (optional, default identity). Projects the slice to track.
- **`compare`** — `(a: TSelected, b: TSelected) => boolean` (optional, default `===`). Equality check used to drop redundant updates.

## Tag variable

The current selected value, typed `TSelected | undefined`. It is `undefined` only at the brief moment on resume before a context-mode provider has parked its bundle; the tag recovers automatically and updates once the bundle is available.

## Behavior

Passing both `from` and `context` throws. The value is seeded at render (null-tolerant in context mode), then kept live by a subscription established on mount. In context mode the tag also listens on the internal publish bus, so it attaches to the store the moment the provider parks it during streaming or resume. Changing the source or swapping the `selector` re-projects; `compare` gates only store mutations, never a deliberate selector change.
31 changes: 31 additions & 0 deletions docs/framework/marko/reference/tags/stream-store-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
id: stream-store-provider
title: "<stream-store-provider>"
---

# Tag: `<stream-store-provider>`

A provider for progressive streaming: it creates a per-request store bundle from data that becomes available inside a late `<await>`, and keeps it live across the server-to-client boundary. Place it inside the awaited body so the awaited value is captured into `value`. Wraps its content.

```marko
<await|order|=loadOrder(orderId)>
<stream-store-provider key="order" value=() => ({ order: createStore(order) })>
<store-selector/total context=(b) => b.order selector=(o) => o.total>
<p>Total: ${total}</p>
</stream-store-provider>
</await>
```

## Attributes

- **`value`** — `() => Record<string, unknown>` (required). A thunk returning the bundle of stores or atoms, built from the awaited per-request data. Called on the server at render and again on the client at mount.
- **`key`** — `string` (**required**). The context key the bundle is parked under. Unlike `<store-provider>`, there is no default; each streamed provider needs an explicit, distinct key.
- **`content`** — `Marko.Body` (optional). The body rendered inside the provider.

## Tag variable

None. Like `<store-provider>`, it renders its content and binds no tag variable.

## Behavior

This is the "born-with-data" transport. The awaited value is captured by `value` and crosses to the client through the await subtree's resume scope; the bundle is built into `$global` under the key on the server, and rebuilt and published on mount so client subscribers attach to live instances. It shares `<store-provider>`'s owner marker, so a `<store-provider>` and a `<stream-store-provider>` on the same key detect each other and throw rather than clobber. For production, a streaming Marko app must run under `@marko/run`; a standalone `@marko/vite` production build does not coordinate resume correctly.
25 changes: 25 additions & 0 deletions examples/marko/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Marko examples for TanStack Store

These mirror the sibling adapter examples (react, svelte, …) using
`@tanstack/marko-store`.

Five client-only parity examples — `stores`, `simple`, `atoms`, `store-actions`,
`store-context` — match the scenarios the other frameworks ship. Each is a
browser-only SPA: `vite.config.ts` uses `marko({ linked: false })` (Marko + Vite
is SSR-first, so a client-only build needs `linked: false`), and `src/index.ts`
mounts the app.

Three Marko-specific SSR examples — `ssr-resume`, `ssr-per-request-multi`,
`streaming` — show server render + resume, per-request data rebuilt on the
client, and streaming a store into a late `<await>` (born-with-data, in-order and
out-of-order). Each runs a small Marko Vite dev server (`node server.mjs`); open
the routes named in its README.

Those three SSR servers are development demonstrations, not deployable production
servers. Real Marko production uses `@marko/run`; a dedicated `@marko/run`
example will be added separately.

In this monorepo you do not install per example. Add `examples/marko/**` to
`pnpm-workspace.yaml`, run `pnpm install` once at the root, then start an example
by folder (`cd examples/marko/<name> && pnpm dev`) or by name
(`pnpm --filter @tanstack/store-example-marko-<name> dev`).
10 changes: 10 additions & 0 deletions examples/marko/atoms/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Marko Store — atoms

A module-level `createAtom`. The read-only total uses `<store-selector>` (no
selector function = the whole value); the editable count uses `<store-atom>`,
whose binding writes back through `atom.set`. Reset calls `atom.set(0)`.

To run this example:

- `npm install`
- `npm run dev`
Loading