svatoms
v0.2.0
Published
Svelte model data flow utilities with selectors.
Readme
svatoms
Svelte's utilities for model data flow: provide a model once, then consume slices of it anywhere with minimal updates. The API mirrors the idea of React Jotai “provider + selector”, but uses Svelte context + stores instead of React providers.
Install
pnpm add svatomsWhy this exists
Inspired by Jojoo and Shiro, I wanted a Svelte-native way to handle complex model data flow with:
- a single entry point (one SSR
load()result) - minimal-granularity reads via selectors
- centralized business data to avoid scattered requests
svatoms keeps it idiomatic to Svelte:
- use Svelte
setContext/getContextinstead of provider components - use Svelte stores instead of hooks
- keep SSR-safe local scope by default
- allow optional global sharing
The exact problem this solves (SvelteKit version)
Typical SvelteKit pain points:
load()data needs to be used across many layers- prop-drilling becomes noisy quickly 😭
- many components only need tiny fields, but full models trigger extra updates
svatoms maps the pattern into SvelteKit cleanly:
- single entry: call
mountModelDataonce in+page.svelteor+layout.svelte - minimal slices: child components use
selectModelDatafor only useful fields - no scattered fetching: all data comes from
load()
Minimal SvelteKit wiring:
// src/routes/posts/[slug]/+page.server.ts
export const load = async ({ params }) => {
const post = await fetchPost(params.slug)
return { post }
}// src/lib/post-context.ts
import { createModelDataContext } from 'svatoms'
export type Post = {
title: string
likes: number
// ...
}
export const postContext = createModelDataContext<Post>({ name: 'post' })<!-- src/routes/posts/[slug]/+page.svelte -->
<script lang="ts">
import { postContext } from '$lib/post-context'
let { data } = $props()
// Recommended in Svelte 5 runes mode:
// pass a getter to keep model data in sync automatically.
postContext.mountModelData(() => data.post)
</script>
<slot /><!-- Any child component -->
<script lang="ts">
import { postContext } from '$lib/post-context'
const title = postContext.selectModelData((p) => p?.title ?? '')
const likeCount = postContext.selectModelData((p) => p?.likes ?? 0)
</script>
<h1>{$title}</h1>
<span>{$likeCount}</span>If you want the model to survive nested route changes, mount it in +layout.svelte instead of +page.svelte.
When the model data changes (e.g. user likes a post), use setModelData or updateModelData to update it in one place, and all selectors will update accordingly.
Important: context usage in event handlers
getContext(...) can only be called during component initialization. That means calling
setModelData / updateModelData / getModelData directly inside event handlers will throw.
Bind actions once during initialization, then use them in events:
<script lang="ts">
import { postContext } from '$lib/post-context'
const { updateModelData } = postContext.useModelActions() // don't forget this!
const like = () => {
updateModelData((prev) =>
prev ? { ...prev, likes: prev.likes + 1 } : prev
)
}
</script>SvelteKit data flow (SSR → UI)
1) Load data on the server
// +page.server.ts
export const load = async ({ params }) => {
const post = await fetchPost(params.slug)
return { post }
}2) Provide the model once
<!-- +page.svelte -->
<script lang="ts">
import { postContext } from '$lib/post-context'
let { data } = $props()
postContext.mountModelData(() => data.post)
</script>
<slot />Why getter? On SvelteKit client-side navigation, the component may stay mounted while only data changes. Passing a getter keeps context store synced automatically.
3) Select slices anywhere
<script lang="ts">
import { postContext } from '$lib/post-context'
const meta = postContext.selectModelData((p) => ({
title: p?.title,
likes: p?.likes ?? 0,
}))
</script>
<h1>{$meta.title}</h1>
<p>Likes: {$meta.likes}</p>4) Update the model from anywhere
<script lang="ts">
import { postContext } from '$lib/post-context'
const { updateModelData } = postContext.useModelActions()
const like = () => {
updateModelData((prev) =>
prev ? { ...prev, likes: prev.likes + 1 } : prev
)
}
</script>
<button onclick={like}>Like</button>Plain Svelte usage
<script lang="ts">
import { createModelDataContext } from 'svatoms'
type User = { id: string; name: string; role: string }
const userContext = createModelDataContext<User>({ name: 'user' })
let user = $state<User>({ id: '1', name: 'Ada', role: 'admin' })
userContext.mountModelData(() => user)
const { updateModelData } = userContext.useModelActions()
const name = userContext.selectModelData((u) => u?.name ?? 'Unknown')
// Update user name
const rename = () => {
updateModelData((prev) =>
prev ? { ...prev, name: 'Grace Hopper' } : prev
)
}
</script>
<p>User: {$name}</p>API
createModelDataContext<Model>(options?)
Create a context manager for a specific model type.
Options
name?: string– used to label the internal symbol (for debugging)key?: symbol– custom context key (advanced)initial?: Model | null– initial value for the global storedefaultScope?: 'local' | 'global'– default store scope
Returns
provideModelData(data, opts?)- Set context + write initial data (no auto cleanup)
mountModelData(dataOrSource, opts?)- Same as
provideModelData, but resets tonullon destroy - Supports static value, getter
() => value, orReadablestore input - Static value input is supported for compatibility, but marked deprecated in TypeScript
- Same as
provideModelStore(store)- Inject a custom store directly
useModelStore(fallback?)- Get the current store (default fallback is global)
useModelActions()- Bind store actions during component initialization (safe for events)
selectModelData(selector, opts?)- Create a derived store from a selector
setModelData(value)/updateModelData(fn)/getModelData()- Convenience helpers for the current store
setGlobalModelData(valueOrUpdater)/getGlobalModelData()- Global store helpers
syncModelData(store, data)- Explicitly push new data into a store
selectModelData options
selectModelData(selector, {
equals?: (a, b) => boolean // default: Object.is
})
// selector: (model: Model | null) => ResultUse equals to avoid re-renders when your selector returns derived objects.
Local vs global scope
- local (default): data is scoped to the component tree that called
mountModelData. This is SSR-safe. - global: data is shared across the whole app, similar to a singleton store.
You can override per call:
postContext.mountModelData(() => data.post, { scope: 'global' })Notes
- Call
mountModelDataduring component initialization (top-level of<script>), not inside functions. - Svelte 5 runes mode uses
$effect; in Svelte 4 you can use$:instead. - In Svelte 5 runes mode, prefer
mountModelData(() => data.xxx)to avoidstate_referenced_locallywarnings. - If you pass a static value, use
syncModelDataorstore.set()when your data changes after navigation.
License
MIT License © 2026 grtsinry43
