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.
pnpm add svatoms
Inspired by Jojoo and Shiro, I wanted a Svelte-native way to handle complex model data flow with:
load() result)svatoms keeps it idiomatic to Svelte:
setContext/getContext instead of provider componentsTypical SvelteKit pain points:
load() data needs to be used across many layerssvatoms maps the pattern into SvelteKit cleanly:
mountModelData once in +page.svelte or +layout.svelteselectModelData for only useful fieldsload()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>
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>
<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>
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 scopeReturns
provideModelData(data, opts?)mountModelData(dataOrSource, opts?)provideModelData, but resets to null on destroy() => value, or Readable store inputprovideModelStore(store)useModelStore(fallback?)useModelActions()selectModelData(selector, opts?)setModelData(value) / updateModelData(fn) / getModelData()setGlobalModelData(valueOrUpdater) / getGlobalModelData()syncModelData(store, data)selectModelData optionsselectModelData(selector, {
equals?: (a, b) => boolean // default: Object.is
})
// selector: (model: Model | null) => Result
Use equals to avoid re-renders when your selector returns derived objects.
mountModelData. This is SSR-safe.You can override per call:
postContext.mountModelData(() => data.post, { scope: 'global' })
mountModelData during component initialization (top-level of <script>), not inside functions.$effect; in Svelte 4 you can use $: instead.mountModelData(() => data.xxx) to avoid state_referenced_locally warnings.syncModelData or store.set() when your data changes after navigation.MIT License © 2026 grtsinry43