diff --git a/README.md b/README.md
index 3043cc5b..db5989f1 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ A lightweight, modular, and performant state management ecosystem for building m
| [`@nano_kit/query`](packages/query#readme) | Data fetching and caching library, built on @nano_kit/store. | [![NPM version][query-npm]][query-npm-url] |
| [`@nano_kit/intl`](packages/intl#readme) | Internationalization library, built on @nano_kit/store. | [![NPM version][intl-npm]][intl-npm-url] |
| [`@nano_kit/ssr`](packages/ssr#readme) | Base package for server-side rendering capabilities. | [![NPM version][ssr-npm]][ssr-npm-url] |
+| [`@nano_kit/devtools`](packages/devtools#readme) | In-page DevTools panel for @nano_kit/store. | [![NPM version][devtools-npm]][devtools-npm-url] |
| [`@nano_kit/react`](packages/react#readme) | React integration for @nano_kit/store. | [![NPM version][react-npm]][react-npm-url] |
| [`@nano_kit/react-router`](packages/react-router#readme) | React integration for @nano_kit/router. | [![NPM version][react-router-npm]][react-router-npm-url] |
| [`@nano_kit/react-ssr`](packages/react-ssr#readme) | React adapter for server-side rendering capabilities. | [![NPM version][react-ssr-npm]][react-ssr-npm-url] |
@@ -77,6 +78,11 @@ See the [Agent Skills](https://nano-kit.js.org/getting-started/agent-skills/) pa
[ssr-npm]: https://img.shields.io/npm/v/%40nano_kit%2Fssr.svg
[ssr-npm-url]: https://npmjs.com/package/@nano_kit/ssr
+
+
+[devtools-npm]: https://img.shields.io/npm/v/%40nano_kit%2Fdevtools.svg
+[devtools-npm-url]: https://npmjs.com/package/@nano_kit/devtools
+
[react-npm]: https://img.shields.io/npm/v/%40nano_kit%2Freact.svg
diff --git a/examples/README.md b/examples/README.md
index 772caac3..b3a99524 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -6,17 +6,17 @@ This document contains bundle size information for various examples in the proje
| Stack | Raw Size | Gzipped Size |
|-------|----------|--------------|
-| Solid + Nano Stores | 36.44 kB | 13.68 kB 🪶 |
-| Svelte + Nano Stores | 56.46 kB | 21.69 kB |
-| React + Nano Stores | 206.77 kB | 65.90 kB |
-| React + nano_kit | 211.32 kB | 67.25 kB |
-| React + nano_kit + DI | 212.49 kB | 67.60 kB |
-| React + Reatom | 228.16 kB | 73.00 kB |
-| React + TanStack Query | 232.63 kB | 72.42 kB 🪨 |
+| Solid + Nano Stores | 36.41 kB | 13.92 kB 🪶 |
+| Svelte + Nano Stores | 57.55 kB | 22.20 kB |
+| React + Nano Stores | 206.71 kB | 65.53 kB |
+| React + nano_kit | 211.50 kB | 66.93 kB |
+| React + nano_kit + DI | 213.03 kB | 67.52 kB |
+| React + Reatom | 228.10 kB | 72.57 kB |
+| React + TanStack Query | 232.56 kB | 71.97 kB 🪨 |
## rick-and-morty 🛸
| Stack | Raw Size | Gzipped Size |
|-------|----------|--------------|
-| React + nano_kit query + nano_kit router | 229.85 kB | 74.48 kB 🪶 |
-| React + TanStack Query + TanStack Router | 330.61 kB | 103.44 kB 🪨 |
+| React + nano_kit query + nano_kit router | 218.76 kB | 73.34 kB 🪶 |
+| React + TanStack Query + TanStack Router | 319.97 kB | 101.70 kB 🪨 |
diff --git a/packages/agera/README.md b/packages/agera/README.md
index 9b0f961b..7b1c8e4e 100644
--- a/packages/agera/README.md
+++ b/packages/agera/README.md
@@ -43,10 +43,10 @@ Was created as reactivity system for [Nanoviews](https://github.com/TrigenSoftwa
import { signal, computed, effect } from 'agera'
const $count = signal(1)
-const $doubleCount = computed(() => count() * 2)
+const $doubleCount = computed(() => $count() * 2)
effect(() => {
- console.log(`Count is: ${$count()}`);
+ console.log(`Count is: ${$count()}`)
}) // Console: Count is: 1
console.log($doubleCount()) // 2
@@ -276,8 +276,6 @@ effect(() => {
untracked(() => {
const b = $b()
- resumeTracking()
-
console.log('Sum:', a + b)
})
})
diff --git a/packages/devtools/README.md b/packages/devtools/README.md
index b386007b..3eeebe25 100644
--- a/packages/devtools/README.md
+++ b/packages/devtools/README.md
@@ -69,4 +69,4 @@ Open the panel with the pill at the bottom edge of the page, or with Alt+Shift+D
## Documentation
-For guides and API reference, visit the [documentation website](https://nano-kit.js.org/devtools).
+For guides and API reference, visit the [documentation website](https://nano-kit.js.org/store/devtools/).
diff --git a/packages/kida/README.md b/packages/kida/README.md
index da244577..ff72ad75 100644
--- a/packages/kida/README.md
+++ b/packages/kida/README.md
@@ -386,10 +386,10 @@ console.log($atId4()) // John
$atId4('Bob')
-console.log($atId4()) // { 2: 'Dan', 4: 'Bob', 6: 'Alice' }
+console.log($users()) // { 2: 'Dan', 4: 'Bob', 6: 'Alice' }
```
-`atKey` supports dynamic indexes.
+`atKey` supports dynamic keys.
```ts
import { signal, atKey } from 'kida'
@@ -404,7 +404,7 @@ const $user = atKey($users, $id)
console.log($user()) // John
-$index(6)
+$id(6)
console.log($user()) // Alice
```
@@ -414,6 +414,21 @@ There are also other methods to work with object maps:
- `setKey($object, key, value)` - set value by key to the object signal.
- `deleteKey($object, key)` - delete item by key from the object signal.
+### `child`
+
+`child` method creates a signal for a property of a parent signal with a custom writer: `atIndex` and `atKey` are built on it. The writer gets the parent value, the key and the new value, and returns the new parent value. The child is writable when the parent is writable, and read-only otherwise.
+
+```ts
+import { signal, child, assignKey } from 'kida'
+
+const $user = signal({ name: 'Dan', age: 30 })
+const $name = child($user, 'name', assignKey)
+
+$name('Alice')
+
+console.log($user()) // { name: 'Alice', age: 30 }
+```
+
## Dependency injection
The dependency injection system enables modular architecture and makes testing easier by allowing dependencies to be easily replaced with mocks. It also plays a critical role in SSR scenarios by isolating state between requests.
@@ -499,6 +514,56 @@ run(context, () => {
})
```
+### Nested contexts
+
+Pass a parent as the second argument of `InjectionContext` to create a child context. A child sees everything provided above it, and its own providers override the ones above.
+
+A dependency that is not provided is created once and shared as widely as its inputs allow: it lives in the deepest context that provides something it reads, directly or through the dependencies it injects, and in the top context when it reads nothing provided. Every context below takes it from there:
+
+```ts
+import { InjectionContext, provide, inject } from 'kida'
+
+function Theme$() {
+ return 'light'
+}
+
+function Palette$() {
+ return { theme: inject(Theme$) }
+}
+
+function Session$() {
+ return { user: null }
+}
+
+const app = new InjectionContext()
+const dark = new InjectionContext([provide(Theme$, 'dark')], app)
+
+inject(Session$, dark) === inject(Session$, app) // true, Session$ reads nothing that dark provides
+inject(Palette$, dark) === inject(Palette$, app) // false, dark has its own Palette$ for its Theme$
+```
+
+### `provideAs`
+
+`provideAs` resolves a token as another injectable. Unlike `provide`, which takes a ready value, it takes an injectable: the alias is created on first use, injects its own dependencies, and the token and the alias share one instance:
+
+```ts
+import { InjectionContext, provideAs, inject } from 'kida'
+
+function Api$() {
+ return { getUser: (id: number) => fetch(`/user/${id}`).then(response => response.json()) }
+}
+
+function MockApi$() {
+ return { getUser: (id: number) => Promise.resolve({ id, name: 'Test User' }) }
+}
+
+const context = new InjectionContext([provideAs(Api$, MockApi$)])
+
+inject(Api$, context) === inject(MockApi$, context) // true
+```
+
+When a token is provided twice in one array, the last provider wins, for values and aliases alike.
+
## Utils
### `isSignal`
@@ -544,6 +609,30 @@ const $user = signal(null)
const $hasUser = boolean($user)
```
+### `latest`
+
+`latest` method creates a signal with the value of the source that changed last. For example, a search field paired with a URL parameter: typing wins over the URL, navigation wins over what was typed.
+
+```ts
+import { signal, latest } from 'kida'
+
+const $searchQuery = signal('') // search field
+const $searchParam = signal('kida') // URL parameter
+const $search = latest($searchQuery, $searchParam)
+
+console.log($search()) // kida
+
+$searchQuery('agera')
+
+console.log($search()) // agera
+
+$searchParam('nano')
+
+console.log($search()) // nano
+```
+
+On the first read, and when several sources changed since the previous read (e.g. inside a `batch`), the last source in the argument list wins. Writing a value equal to the current one is not a change.
+
### `$get`
`$get` method gets the value from the signal or returns the given value.
@@ -611,7 +700,9 @@ const [$position, $error, $pending] = resolved(
)
```
-A falsy source resets all signals to their initial state (`data: undefined`, `error: undefined`, `pending: false`).
+A source value that is not a promise, a falsy one included, becomes the result as is. Express an empty state with the type, e.g. `T | null`.
+
+Each promise from the source is attached as a task, so `waitTasks` of any of the three signals waits until it settles. Like any computed, a signal of the tuple links the source only on its first read: read it before waiting.
> [!NOTE]
> For remote data fetching with caching, request deduplication, cancellation, refetching, and other advanced features, consider using [`@nano_kit/query`](https://github.com/TrigenSoftware/nano_kit/tree/main/packages/query) instead.
@@ -626,6 +717,6 @@ Nano Stores is a great library with wonderful idea of stores with lifecycles. Bu
| Benchmark
Throughput avg (ops/s) | Kida / Agera | Alien Signals | Nano Stores |
| ------- | --------- | ------- | ------ |
-| [signal](../benchmarks/atom.js) | 17 670 321 ± 0.01% | 19 160 348 ± 0.00% | 1 364 731 ± 0.03% |
-| [computed](../benchmarks/computed.js) | 1 193 380 ± 0.02% | 1 307 870 ± 0.02% | 229 406 ± 0.14% |
-| [effect](../benchmarks/effect.js) | 3 309 252 ± 0.01% | 3 441 332 ± 0.01% | 922 683 ± 0.04% |
+| [signal](../benchmarks/atom.js) | 15 048 243 ± 0.01% | 18 967 631 ± 0.00% | 1 389 555 ± 0.02% |
+| [computed](../benchmarks/computed.js) | 1 208 888 ± 0.02% | 1 331 229 ± 0.02% | 187 524 ± 0.15% |
+| [effect](../benchmarks/effect.js) | 3 257 518 ± 0.01% | 3 463 918 ± 0.01% | 955 623 ± 0.03% |
diff --git a/packages/ssr/README.md b/packages/ssr/README.md
index c84f13c9..5fe11287 100644
--- a/packages/ssr/README.md
+++ b/packages/ssr/README.md
@@ -45,7 +45,7 @@ Subclass `Renderer` and implement the abstract `renderToString` method. The cons
```js
// src/renderer/index.js
-import { Renderer, headDescriptorToHtml, ROOT_ID } from '@nano_kit/ssr/renderer'
+import { Renderer, headDescriptorToHtml, escapeHtml, ROOT_ID } from '@nano_kit/ssr/renderer'
import { compose } from 'your-framework-router'
import { get } from '@nano_kit/store'
@@ -64,12 +64,12 @@ export class FrameworkRenderer extends Renderer {
else head += headDescriptorToHtml(descriptor)
})
- if (title) head = `
${title}${head}`
+ if (title) head = `${escapeHtml(title)}${head}`
// render to string using your framework, wrapping with data.context
const body = yourFrameworkRenderToString(data.context)
- return `${head}${body}
`
+ return `${head}${body}
`
}
}
```
diff --git a/packages/store/README.md b/packages/store/README.md
index 9039da5a..f9b4107f 100644
--- a/packages/store/README.md
+++ b/packages/store/README.md
@@ -52,10 +52,10 @@ yarn add @nano_kit/store
Here is a minimal example demonstrating signals, computed values, and effects in action:
```ts
-import { signal, onMount, computed, effect } from '@nano_kit/store'
+import { signal, mountable, onMount, computed, effect } from '@nano_kit/store'
-/* Create independent atomic stores */
-const $count = signal(1)
+/* Create independent mountable atomic store */
+const $count = mountable(signal(1))
/* Mountable: Run logic only when store has listeners */
onMount($count, () => {
@@ -76,6 +76,7 @@ const unsub = effect(() => {
console.log(`Count: ${$count()}, Double: ${$double()}`)
})
// Output: Count: 1, Double: 2
+// Output: Mounted: Store is active
/* Update triggers granular propagation */
$count(2)
diff --git a/skills/nano-kit-intl/DOCS.md b/skills/nano-kit-intl/DOCS.md
index 79a703cb..0cf1678f 100644
--- a/skills/nano-kit-intl/DOCS.md
+++ b/skills/nano-kit-intl/DOCS.md
@@ -283,7 +283,7 @@ const [$t, $pending, $error] = messages('common', {
})
```
-Use `$t()` to read the whole namespace. Messages are also available as signal properties like `$t.$title`. For parameterized messages, `$t.key(params)` creates a computed signal. If params contain signals, the computed message subscribes to them.
+Use `$t()` to read the whole namespace. Messages are also available as signal properties like `$t.$title`. For parameterized messages, `$t.key(params)` creates a computed signal. If params contain signals, the computed message subscribes to them, and the value of a `format()` message can be a signal or an accessor itself.
```ts
$t().title
@@ -483,6 +483,20 @@ $t().eventDate(new Date('2024-01-02T00:00:00.000Z'))
This is useful for dates, numbers, and other UI values that come from app state.
+The formatter reads a signal or an accessor passed as the value, so the `$t.eventDate($startsAt)` shortcut is a computed message that follows `$startsAt`. A `null` value formats like a missing one, so data that is still loading needs no guard:
+
+```ts
+const $startsAt = signal(null)
+const $eventDate = $t.eventDate($startsAt)
+
+$eventDate()
+// undefined
+
+$startsAt(new Date('2024-01-02T00:00:00.000Z'))
+$eventDate()
+// 'Jan 2, 2024'
+```
+
## Intl Formats
Intl formats can be used inside `params(...)` for translated templates, or wrapped with `format(...)` when the value comes from runtime state, API data, or a database.
@@ -522,20 +536,35 @@ $t().formatDate(new Date('2024-01-02T00:00:00.000Z'))
### `relativetime(fallback?, options?)`
-Formats relative time values with `Intl.RelativeTimeFormat`.
+Formats the distance from now to a date, a timestamp or a date string with `Intl.RelativeTimeFormat`, in the largest unit the distance fills: years, months, weeks, days, hours, minutes or seconds.
```ts
const [$t] = messages('event', {
formatStartsIn: format(relativetime({
- unit: 'day',
numeric: 'auto'
}))
})
-$t().formatStartsIn(1)
-// 'tomorrow'
+$t().formatStartsIn(event.startsAt)
+// 'tomorrow' for an event that starts in 30 hours
```
+`largestUnit` and `smallestUnit` bound the unit, and both set to one unit format in that unit only. With `numeric: 'auto'`, a distance below the smallest unit reads as "now" when that unit is an hour or less, and as "today" or "this week" for calendar units, so a feed with `smallestUnit: 'minute'` shows "now" for anything newer than a minute:
+
+```ts
+const [$t] = messages('activity', {
+ formatTime: format(relativetime({
+ numeric: 'auto',
+ smallestUnit: 'minute'
+ }))
+})
+
+$t().formatTime(comment.createdAt)
+// 'now', then '5 minutes ago', 'yesterday', 'last week'
+```
+
+`relativeTo` sets the moment to count from instead of the current time. It takes a signal or an accessor as well, so a message follows a ticking clock, for example one built on `interval()`. A month counts as 30 days and a year as 365: the unit comes from the distance, not from the calendar.
+
### `duration(fallback?, options?)`
Formats duration objects with `Intl.DurationFormat`.
diff --git a/skills/nano-kit-platform-web/DOCS.md b/skills/nano-kit-platform-web/DOCS.md
index 6aaab4b3..516936d2 100644
--- a/skills/nano-kit-platform-web/DOCS.md
+++ b/skills/nano-kit-platform-web/DOCS.md
@@ -327,7 +327,7 @@ const context = [
]
```
-`parseLocales` sorts languages by `q` quality and keeps declaration order when qualities are equal. Empty headers fall back to `{ language: 'en', languages: ['en'] }`.
+`parseLocales` sorts languages by `q` quality and keeps declaration order when qualities are equal. An empty or missing header (`null` or `undefined`) falls back to `{ language: 'en', languages: ['en'] }`.
## User Agent
diff --git a/skills/nano-kit-query/DOCS.md b/skills/nano-kit-query/DOCS.md
index c25fcbee..f5a6b000 100644
--- a/skills/nano-kit-query/DOCS.md
+++ b/skills/nano-kit-query/DOCS.md
@@ -333,6 +333,8 @@ SearchKey('react', 2)
This allows different page requests to share the same cache entry.
+The filter is also the place to drop or map a parameter that does not serialize, such as a function or a symbol: it would become `null` in the key, and the development build warns about it.
+
### Using Cache Keys with Client Methods
#### Reading and Writing Data
@@ -393,7 +395,7 @@ revalidate(PostKey)
/* All active post queries will refetch */
```
-**What happens**: Revalidation doesn't remove data from cache, it just marks it as stale. Active queries (those with listeners) will automatically refetch.
+**What happens**: Revalidation doesn't remove data from cache, it just marks it as stale. Active queries (those with listeners) will automatically refetch. If an active query still has a request in flight, the revalidation restarts it, and only the response of the restarted request reaches the cache.
#### Invalidating Cache
@@ -672,22 +674,24 @@ You do not need this for ordinary in-memory browser cache usage. While the app i
Use it when serialized cache entries contain values that need custom encoding, such as `Date`, `Map`, `Set`, class instances, or encrypted/compact payloads.
-`hydratable()`, `ssr()`, and [`persistence()`](#persistence) use the codec when they dehydrate data, hydrate it back, write it to persistent storage, or read it back.
+`hydratable()` and [`persistence()`](#persistence) use the codec when they dehydrate data, hydrate it back, write it to persistent storage, or read it back.
```ts
import type { Codec } from '@nano_kit/store'
import { parse, stringify } from 'devalue'
-import { client, codec, ssr } from '@nano_kit/query'
+import { client, codec, hydratable } from '@nano_kit/query'
const DevalueCodec: Codec = {
encode: stringify,
decode: value => value === null ? null : parse(value)
}
-const { query } = client(
- codec(DevalueCodec),
- ssr()
-)
+export function Client$() {
+ return client(
+ codec(DevalueCodec),
+ hydratable()
+ )
+}
```
With a codec like this, data containing values such as `Date`, `Map`, or `Set` can be dehydrated on the server and restored on the client without losing those types.
@@ -758,6 +762,8 @@ keys(invalidate)
This is useful after broad application changes, such as sign in, sign out, locale changes, or any mutation that can affect several query domains at once.
+The name of a builder is its cache shard: two builders with one name share one cache.
+
## Cache Mutation Helpers
For comfortable cache mutations, use helpers that fit naturally into `$data` reducers. They are especially handy for optimistic updates.
@@ -1224,31 +1230,6 @@ The `capture` helper stores each entity in the shared entity cache and returns a
**When to use:** Complex applications where the same data (e.g., a "User" or "Product") appears in multiple places or lists and needs to stay synchronized.
-### `tasks`
-
-Integrates with `@nano_kit/store`'s task tracking system. This is mainly used for server-side rendering (SSR) to wait for all data fetches to complete before rendering the HTML.
-
-```ts
-import { tasksRunner, waitTasks } from '@nano_kit/store'
-import { client, tasks } from '@nano_kit/query'
-
-const tasksPool = new Set()
-const runTask = tasksRunner(tasksPool)
-
-const { query } = client(
- tasks(runTask)
-)
-
-/* ... application runs ... */
-
-/* Wait for all queries to finish */
-await waitTasks(tasksPool)
-```
-
-Without arguments, `tasks()` reads `TasksRunner$` from the current injection context. Outside DI, pass the runner explicitly.
-
-**When to use:** SSR setups where the server should send a fully populated page to the client.
-
---
# SSR
@@ -1257,54 +1238,33 @@ Learn how to use @nano_kit/query for server-side rendering (SSR).
Server-side rendering in `@nano_kit/query` is built around two pieces: task tracking and cache dehydration.
-Queries started during server render should be awaited before HTML is returned, and the resulting cache state should be transferred to the client for hydration.
+Queries started during server render should be awaited before HTML is returned, and the resulting cache state should be transferred to the client for hydration. Task tracking is built in: every query attaches its request as a task to its data signal, so there is nothing to set up for it. Cache dehydration is the [`hydratable()`](#hydratable) setting.
## `hydratable`
-Use `hydratable()` when query cache should be dehydrated on the server and rehydrated on the client.
+Use `hydratable()` when query cache should be dehydrated on the server and rehydrated on the client. It is the whole SSR setting of a query client.
Without arguments, it reads hydration dependencies from the current injection context. Outside DI, pass them explicitly.
-In a manual setup, it is typically paired with `tasks()`, which works the same way: without arguments it reads `TasksRunner$` from the current injection context, and outside DI you can pass the runner explicitly.
-
If the client uses `codec(...)`, `hydratable()` encodes cached data before dehydration and decodes it during hydration.
```ts
-import { client, tasks, hydratable } from '@nano_kit/query'
-
-const { query } = client(
- tasks(),
- hydratable()
-)
-```
-
-Use this when you want to customize task handling or hydration setup independently.
-
-## `ssr`
-
-`ssr()` is a convenience setting that combines `tasks()` and `hydratable()`.
-
-Inside an SSR injection context, it reads the task runner and hydration dependencies automatically. If you need explicit control instead, use `tasks(...)` and `hydratable(...)` directly.
-
-Because `ssr()` uses `hydratable()` internally, it also respects the current `codec(...)` setting.
-
-```ts
-import { client, dedupeTime, ssr } from '@nano_kit/query'
+import { client, dedupeTime, hydratable } from '@nano_kit/query'
const DEDUPE_TIME = 300_000 // 5 minutes
export function Client$() {
return client(
dedupeTime(DEDUPE_TIME),
- ssr()
+ hydratable()
)
}
```
This enables the usual SSR flow:
-1. Queries started during server render are tracked as tasks.
-2. The renderer waits for those tasks to finish.
+1. Queries started during server render attach their requests as tasks to their data signals.
+2. The renderer waits for the tasks of the signals the page returns from `Stores$`, and of everything they depend on.
3. Query cache state is dehydrated into the hydration payload.
4. The client rehydrates the cache and reuses the prefetched data.
@@ -1324,28 +1284,23 @@ Please refer to the Store Testing documentation for general guidelines on:
## Waiting for Queries
-Tests often need to wait for asynchronous data fetching to resolve. The `tasks` extension provides a reliable way to await all pending operations in your query client, ensuring your assertions run only after data is loaded.
+Tests often need to wait for asynchronous data fetching to resolve. Every query attaches its request as a task to its data signal, so `waitTasks` of that signal awaits the request with no extra setup, ensuring your assertions run only after data is loaded.
```ts
import { describe, it, expect, vi } from 'vitest'
-import { tasksRunner, waitTasks, start } from '@nano_kit/store'
-import { client, tasks, queryKey } from '@nano_kit/query'
+import { waitTasks, start } from '@nano_kit/store'
+import { client, queryKey } from '@nano_kit/query'
const TestKey = queryKey('test')
it('should fetch and update state', async () => {
- /* 1. Create a pool to track active tasks */
- const tasksPool = new Set()
-
- /* 2. Configure client with tasks extension */
- const { query } = client(
- tasks(tasksRunner(tasksPool))
- )
+ /* 1. Create a client, no task setup is needed */
+ const { query } = client()
/* Mock fetcher */
const fetcher = vi.fn().mockResolvedValue('success')
- /* 3. Start the query */
+ /* 2. Start the query */
const [$data, $error, $loading] = query(TestKey, [], fetcher)
/* Mount the signal to trigger the fetch */
@@ -1353,13 +1308,15 @@ it('should fetch and update state', async () => {
expect($loading()).toBe(true)
- /* 4. Wait for all fetch tasks to complete */
- await waitTasks(tasksPool)
+ /* 3. Wait for the request of $data to complete */
+ await waitTasks($data)
- /* 5. Assert final state */
+ /* 4. Assert final state */
expect($loading()).toBe(false)
expect($data()).toBe('success')
stop()
})
```
+
+`waitTasks` of any signal computed from `$data` waits for the request as well. Infinite queries and operations attach their requests to `$data` too. A mutation attaches each request to all of its signals, so waiting for any of them works, as does awaiting the promise returned by `mutate`.
diff --git a/skills/nano-kit-react-router/DOCS.md b/skills/nano-kit-react-router/DOCS.md
index 819347ed..2329958c 100644
--- a/skills/nano-kit-react-router/DOCS.md
+++ b/skills/nano-kit-react-router/DOCS.md
@@ -611,6 +611,18 @@ export function Head$() {
}
```
+### `toHtmlAttribute`
+
+Descriptor props are camelCase, like `httpEquiv` or `crossOrigin`. `toHtmlAttribute` maps a prop name to its HTML attribute name, the same way `syncHead` and the SSR renderer do. Use it when a custom adapter turns descriptor props into attributes itself.
+
+```ts
+import { toHtmlAttribute } from '@nano_kit/router'
+
+toHtmlAttribute('httpEquiv') // 'http-equiv'
+toHtmlAttribute('crossOrigin') // 'crossorigin'
+toHtmlAttribute('content') // 'content'
+```
+
## Scroll Management
Since scroll restoration and behavior can vary significantly between applications, the library provides a set of "Do It Yourself" (DIY) utilities instead of a one-size-fits-all solution. You can combine these tools to implement the exact scrolling behavior your app needs.
diff --git a/skills/nano-kit-react-ssr/DOCS.md b/skills/nano-kit-react-ssr/DOCS.md
index c34ae94f..2e928c2a 100644
--- a/skills/nano-kit-react-ssr/DOCS.md
+++ b/skills/nano-kit-react-ssr/DOCS.md
@@ -580,4 +580,4 @@ browserLocale(context.get(Locales$), ['en', 'ru-RU'], 'en')
// 'ru-RU'
```
-`parseLocales` sorts languages by `q` quality and keeps declaration order when qualities are equal. Empty headers fall back to `{ language: 'en', languages: ['en'] }`.
+`parseLocales` sorts languages by `q` quality and keeps declaration order when qualities are equal. An empty or missing header (`null` or `undefined`) falls back to `{ language: 'en', languages: ['en'] }`.
diff --git a/skills/nano-kit-react/DOCS.md b/skills/nano-kit-react/DOCS.md
index 2ddb146f..34c94479 100644
--- a/skills/nano-kit-react/DOCS.md
+++ b/skills/nano-kit-react/DOCS.md
@@ -45,6 +45,8 @@ To use the Dependency Injection system within React, wrap your application (or p
You can provide an existing context instance or an array of providers/values.
+Inside another provider, one with an array of providers creates a child context, and one without `context` reuses the parent. Which context a store is created in is explained in Nested Contexts.
+
```tsx
import { provide } from '@nano_kit/store'
import { InjectionContextProvider } from '@nano_kit/react'
@@ -152,7 +154,7 @@ import { User$ } from './stores'
function Stores$() {
const { $user } = inject(User$)
- /* Will execute store logic, wait for all async tasks to complete, and extract the dehydrated snapshot */
+ /* Will execute store logic, wait for the async tasks of these stores to complete, and extract the dehydrated snapshot */
return [$user]
}
@@ -179,7 +181,7 @@ import { User$ } from './stores'
function Stores$() {
const { $user } = inject(User$)
- /* Will execute store logic, wait for all async tasks to complete, and extract the dehydrated snapshot */
+ /* Will execute store logic, wait for the async tasks of these stores to complete, and extract the dehydrated snapshot */
return [$user]
}
@@ -218,7 +220,7 @@ import { User$ } from './stores'
function Stores$() {
const { $user } = inject(User$)
- /* Will execute store logic, wait for all async tasks to complete, and extract the dehydrated snapshot */
+ /* Will execute store logic, wait for the async tasks of these stores to complete, and extract the dehydrated snapshot */
return [$user]
}
diff --git a/skills/nano-kit-store/DOCS.md b/skills/nano-kit-store/DOCS.md
index 4aedc05a..941c0bde 100644
--- a/skills/nano-kit-store/DOCS.md
+++ b/skills/nano-kit-store/DOCS.md
@@ -364,15 +364,15 @@ The Dependency Injection system enables modular architecture and makes testing e
Use an injectable function or class with `inject` to retrieve dependencies. For example, in React, use `InjectionContextProvider` to create a context and `useInject` to access dependencies.
```tsx
-import { inject, signal, mountable, onMountEffect, action, effect, TasksRunner$ } from '@nano_kit/store'
+import { signal, mountable, onMountEffect, action, task } from '@nano_kit/store'
import { InjectionContextProvider, useInject, useSignal } from '@nano_kit/react'
/* Injectable function that defines a user store */
function User$() {
- const task = inject(TasksRunner$)
const $userId = signal(null)
const $user = mountable(signal(null))
- const fetchUser = action((id) => task(async () => {
+ /* Attach the request to $user, so SSR and tests can wait for it */
+ const fetchUser = action((id) => task($user, async () => {
if (typeof id !== 'number') {
$user(null)
return
@@ -476,6 +476,89 @@ function App() {
}
```
+### Aliases
+
+`provideAs` resolves a token as another injectable. Unlike `provide`, which takes a ready value, it takes an injectable: the alias is created on first use, injects its own dependencies, and the token and the alias share one instance. It suits implementations swapped per environment, such as a mock API in stories and tests.
+
+```tsx
+import { provideAs } from '@nano_kit/store'
+import { InjectionContextProvider } from '@nano_kit/react'
+
+function Api$() {
+ return {
+ getUser: (id: number) => fetch(`/user/${id}`).then(response => response.json())
+ }
+}
+
+function MockApi$() {
+ return {
+ getUser: (id: number) => Promise.resolve({ id, name: 'Test User' })
+ }
+}
+
+function Preview({ children }) {
+ return (
+
+ {children}
+
+ )
+}
+```
+
+Inside `Preview`, every store that injects `Api$` gets the `MockApi$` instance, and `useInject(MockApi$)` returns that same instance. When a token is provided twice in one array, for example defaults followed by overrides, the last provider wins, for values and aliases alike.
+
+### Nested Contexts
+
+Providers nest. A provider with its own values inside another one creates a child context: its subtree sees everything provided above, and its own providers override the ones above.
+
+A dependency that is not provided is created once and shared as widely as its inputs allow. It lives in the deepest context that provides something it reads, directly or through the dependencies it injects, and in the top context when it reads nothing provided. Every context below takes it from there. So a nested provider gets its own instances of exactly the stores that depend on what it overrides, and everything else stays shared.
+
+```tsx
+import { DependencyNotFound, inject, provide, signal } from '@nano_kit/store'
+import { InjectionContextProvider, useInject, useSignal } from '@nano_kit/react'
+
+function TodoId$(): number {
+ throw new DependencyNotFound('TodoId$')
+}
+
+/* Reads nothing provided: one list for the whole app */
+function Todos$() {
+ const $todos = signal([{ id: 1, title: 'Write docs' }])
+
+ return { $todos }
+}
+
+/* Reads TodoId$: one editor for every provider of TodoId$ */
+function TodoEditor$() {
+ const id = inject(TodoId$)
+ const $draft = signal('')
+
+ return { id, $draft }
+}
+
+function TodoList() {
+ const { $todos } = useInject(Todos$)
+ const todos = useSignal($todos)
+
+ return todos.map(todo => (
+
+
+
+ ))
+}
+
+function TodoItem() {
+ /* The editor of this item, created in its provider */
+ const { id, $draft } = useInject(TodoEditor$)
+ /* The list TodoList reads, created in the top context and shared */
+ const { $todos } = useInject(Todos$)
+
+ /* ... */
+}
+```
+
+To start a subtree from scratch, with new instances of every store, wrap it in `Isolate`.
+
## Naming Convention
The library follows consistent naming conventions to distinguish between different types of reactive entities. Highly recommended to follow these conventions in your own code to maintain consistency, improve readability and make reactive dependencies immediately recognizable:
@@ -499,15 +582,16 @@ const value = $get($count)
Injectable tokens that start with an uppercase letter and end with `$` are Dependency Injection tokens.
```ts
-import { Injectable$, TasksRunner$, inject } from '@nano_kit/store'
+import { Injectable$, inject } from '@nano_kit/store'
+import { CookieStore$ } from '@nano_kit/platform-web'
function User$() {
- const task = inject(TasksRunner$)
+ const cookieStore = inject(CookieStore$)
/* ... */
}
class UserService$ extends Injectable$ {
- task = inject(TasksRunner$)
+ cookieStore = inject(CookieStore$)
}
```
@@ -779,33 +863,95 @@ Additional object helpers:
### Signals Map
-`SignalsMap` is a reactive `Map` where each entry's value is a signal.
+`SignalsMap` is a `Map` whose values are reactive: every value lives in a signal of its own, and the signals never leave the map. It is a class created with `new`, where `get` reads a value without tracking, `$get` reads it and tracks it, and `set`, `delete`, `clear`, `has`, `keys` and `size` work as they do on a `Map`.
```ts
-import { type SignalsMap, $getMapKey, setMapKey, deleteMapKey, clearMap } from '@nano_kit/store'
+import { SignalsMap, effect } from '@nano_kit/store'
-const userMap: SignalsMap = new Map()
+interface User {
+ id: number
+ name: string
+}
+
+const users = new SignalsMap()
/* Set entry */
-setMapKey(userMap, 1, { name: 'Dan', age: 30 })
+users.set(1, { id: 1, name: 'Dan' })
/* Get entry reactively */
effect(() => {
- console.log('User:', $getMapKey(userMap, 1))
+ console.log('User:', users.$get(1))
})
+/* Update entry with a reducer, as with a writable signal */
+users.set(1, user => user && { ...user, name: 'Alice' })
+
+/* Get entry without tracking */
+users.get(1)
+
/* Delete entry */
-deleteMapKey(userMap, 1)
+users.delete(1)
/* Clear all */
-clearMap(userMap)
+users.clear()
+```
+
+A reader of a key that is in the map runs again only when its value changes, not when other keys are set. A reader of a missing key runs again when a key is added or removed, so it gets the value as soon as the key is set.
+
+`IndexedSignalsMap` adds `$index`, a computed list of the keys: it changes when a key is added or removed, not when a value is set. `$index` also stands for the whole map in the lifecycle: mark it `mountable` right after the map is created, and every reader of the map mounts it, the readers of `$get` included.
+
+```ts
+import { IndexedSignalsMap, mountable, onMount, effect } from '@nano_kit/store'
+
+const users = new IndexedSignalsMap()
+
+/* Mark the index mountable before anything reads the map */
+onMount(mountable(users.$index), () => {
+ /* Runs when the first reader of the map appears */
+ const socket = new WebSocket('wss://example.com/users')
+
+ socket.onmessage = (event) => {
+ const user: User = JSON.parse(event.data)
+
+ users.set(user.id, user)
+ }
+
+ return () => socket.close()
+})
+
+/* Runs again when a key is added or removed, not when a value is set */
+effect(() => {
+ console.log('User ids:', users.$index())
+})
+
+/* Runs again when user 1 changes, and keeps the map mounted as well */
+effect(() => {
+ console.log('User 1:', users.$get(1))
+})
```
## Functional Operators
-Functional operators (fops) are pure functions that create accessors from other accessors or values. Unlike `computed`, they don't memoize results — they recalculate on every access. For memoization, wrap them in `computed`.
+Functional operators (fops) are pure functions that combine accessors and plain values. Every operator checks its operands: with an accessor among them the result is an accessor, and with none it is the plain value, so a binding made of static values costs no effect. Unlike `computed`, a returned accessor does not memoize: it recalculates on every access. For memoization, wrap it in `computed`.
+
+```ts
+import { type Signalish, signal, is } from '@nano_kit/store'
+
+const $variant = signal('primary')
-These operators accept either static values or accessors, making them flexible for building derived state without immediate caching.
+/* Accessor: an accessor is among the operands */
+const $isPrimary = is($variant, 'primary')
+
+/* boolean: every operand is static */
+const isPrimary = is('primary', 'primary')
+
+/* Signalish: the operand may turn out either way */
+function isPrimaryVariant(variant: Signalish) {
+ return is(variant, 'primary')
+}
+```
+
+The `Fop` type describes the result of an operator: an accessor when an operand is an accessor, the plain value when none can be, and `Signalish` of the value when an operand may be either.
### Logical Operations
@@ -870,6 +1016,48 @@ const $greeting = when(
console.log($greeting()) /* "Hello, Alice" */
```
+### Lookup
+
+**`pick`** looks a value up in a collection by key. The collection is an object or an array, and both it and the key can be static or accessors. An empty key (`null` or `undefined`) picks `undefined`, which suits an optional prop.
+
+```ts
+import { signal, pick } from '@nano_kit/store'
+
+const styles = {
+ primary: 'button_primary',
+ secondary: 'button_secondary'
+}
+const $variant = signal<'primary' | 'secondary'>('primary')
+const $className = pick(styles, $variant)
+
+console.log($className()) /* "button_primary" */
+
+$variant('secondary')
+console.log($className()) /* "button_secondary" */
+
+/* Static operands give the value itself */
+pick(styles, 'primary') /* "button_primary" */
+```
+
+### Text
+
+**`text`** is a template tag that builds a string from a template literal. The values in it are strings, numbers, booleans or bigints, static or accessors, and an empty value (`null` or `undefined`) adds nothing.
+
+```ts
+import { signal, text } from '@nano_kit/store'
+
+const $name = signal('Dan')
+const $greeting = text`Hello, ${$name}!`
+
+console.log($greeting()) /* "Hello, Dan!" */
+
+$name('Alice')
+console.log($greeting()) /* "Hello, Alice!" */
+
+/* Static values give the string itself */
+text`Step ${1} of ${3}` /* "Step 1 of 3" */
+```
+
### Memoization
Since fops don't cache results, wrap them in `computed` for expensive operations:
@@ -927,7 +1115,7 @@ The library provides a set of utility functions for common tasks like rate limit
### Async State
-**`resolved`** unwraps a promise accessor into a `[$result, $error, $pending]` tuple of signals. Stale data is preserved while a new promise is pending, and falsy values reset the state.
+**`resolved`** unwraps a promise accessor into a `[$result, $error, $pending]` tuple of signals. Stale data is preserved while a new promise is pending.
```ts
import { signal, computed, resolved } from '@nano_kit/store'
@@ -944,7 +1132,21 @@ const [$position, $error, $pending] = resolved(
)
```
-A falsy source resets all signals to their initial state (`result: undefined`, `error: undefined`, `pending: false`).
+A source value that is not a promise becomes the result as it is, a falsy one included, so an empty state is expressed by the type itself, for example `User | null`.
+
+```ts
+import { signal, resolved } from '@nano_kit/store'
+
+const $userId = signal(null)
+const [$user] = resolved(() => {
+ const id = $userId()
+
+ /* The result is null until there is a user to fetch */
+ return id === null ? null : fetchUser(id)
+})
+```
+
+Each promise from the source is attached as a task, so `waitTasks` of any of the three signals waits until it settles. A signal of the tuple links the source on its first read, so read or mount it before waiting.
> Note: For remote data fetching with caching, request deduplication, cancellation, refetching, and other advanced features, consider using `@nano_kit/query` instead.
@@ -1060,6 +1262,30 @@ $count(2) /* Changed from 1 to 2 */
$count(3) /* Changed from 2 to 3 */
```
+### Latest Value
+
+**`latest`** merges several sources into one computed that carries the value of the source that changed last. It fits a local cell paired with a read-only source: typing wins over the URL, and navigation wins over what was typed. Where `previous` looks back through a single source, `latest` picks the freshest value across several.
+
+```ts
+import { signal, latest, effect } from '@nano_kit/store'
+
+/* Stands for a read-only source, such as a search param of the URL */
+const $searchParam = signal('shoes')
+/* The local cell the search field writes */
+const $searchQuery = signal('')
+const $search = latest($searchQuery, $searchParam)
+
+effect(() => {
+ console.log('Search:', $search())
+})
+/* Search: shoes */
+
+$searchQuery('boots') /* Search: boots */
+$searchParam('hats') /* Search: hats */
+```
+
+On the first read the last source wins, and when several sources changed since the previous read, as with writes inside one `batch`, the last of them in argument order wins. Only a new value counts as a change: writing a value equal to the current one leaves the result as it is.
+
### Computed Properties
**`length`** creates a computed for the `length` property of arrays or strings.
@@ -1086,22 +1312,6 @@ $user({ name: 'Dan' })
console.log($hasUser()) /* true */
```
-**`concat`** concatenates multiple values or accessors into a string.
-
-```ts
-import { signal, concat, effect } from '@nano_kit/store'
-
-const $firstName = signal('John')
-const $lastName = signal('Doe')
-const $fullName = concat($firstName, ' ', $lastName)
-
-effect(() => {
- console.log($fullName())
-})
-
-$firstName('Jane') /* Jane Doe */
-```
-
### Type Checking
**`isFunction`** checks if a value is a function.
@@ -1327,17 +1537,16 @@ function User$() {
## Working with Tasks
-SSR requires waiting for all async operations to complete before dehydrating. Use `TasksRunner$` to create a task runner that automatically tracks async operations in `TasksPool$`.
+SSR requires waiting for all async operations to complete before dehydrating. Use `task` to attach an async operation to the signal it fills: until the operation settles, `waitTasks` of that signal, and of every signal that depends on it, waits for it.
```ts
-import { inject, signal, hydratable, mountable, onMountEffect, action, TasksRunner$ } from '@nano_kit/store'
+import { signal, hydratable, mountable, onMountEffect, action, task } from '@nano_kit/store'
function User$() {
- const task = inject(TasksRunner$)
const $userId = hydratable('userId', signal(null))
const $user = hydratable('user', mountable(signal(null)))
- const fetchUser = action((id) => task(async () => {
+ const fetchUser = action((id) => task($user, async () => {
if (typeof id !== 'number') {
$user(null)
return
@@ -1357,7 +1566,9 @@ function User$() {
}
```
-The `task` function wraps async operations and adds them to the tasks pool. The `dehydrate` function waits for all tasks in the pool to complete before extracting signal values.
+`task` takes the signal the operation fills (or an array of signals) and a promise or a function that returns one, and returns that promise. `waitTasks($signal)` walks the dependency graph of the signal and waits, wave by wave, until neither the signal nor anything it depends on has tasks left, so a task started by another task and a dependency read only after a value settles are awaited too.
+
+There is no pool to create or provide: tasks live on the signals they fill, so `dehydrate` waits for exactly the stores it starts, and a test waits for exactly the signal it checks. Queries of `@nano_kit/query` and `resolved` attach their tasks by themselves, so stores built on them need no `task` call.
## Manual Hydration Flow
@@ -1365,7 +1576,7 @@ The `task` function wraps async operations and adds them to the tasks pool. The
### Server-Side Dehydration
-On the server, use `dehydrate` to execute your store logic, wait for all async tasks to complete, and extract the dehydrated key-value pairs.
+On the server, use `dehydrate` to execute your store logic, wait for its async tasks to complete, and extract the dehydrated key-value pairs.
```ts
import { dehydrate, inject } from '@nano_kit/store'
@@ -1386,10 +1597,10 @@ const dehydrated = await dehydrate(() => {
The `dehydrate` function:
-1. Creates an injection context with a task pool, or uses the context passed as the second argument
+1. Creates an injection context, or uses the context passed as the second argument
2. Runs your store factories within that context
3. Starts effects to trigger `onMount` callbacks (which start async tasks)
-4. Waits for all tasks in `TasksPool$` to complete
+4. Waits for the tasks of the returned signals and of every signal they depend on
5. Collects all signals marked with `hydratable`
6. Returns the dehydrated key-value pairs
@@ -1434,6 +1645,8 @@ function UserProfile() {
`StaticHydrator` is a one-shot hydrator: it applies values from the initial dehydrated snapshot and discards them after use. For streaming SSR where chunks of dehydrated data arrive after the initial render, use `ActiveHydrator` instead — it exposes a `push(dehydrated)` method to feed additional data reactively.
+In the development build, `StaticHydrator` warns about an id that has no value in the snapshot: the signal was not dehydrated on the server, or the same id is hydrated twice.
+
---
# Testing
@@ -1521,22 +1734,35 @@ expect(mounted).toHaveBeenCalledTimes(1)
## Waiting for Async Operations
-Tasks are useful not only for SSR but also in tests to wait for async operations inside effects. Use tasks pool with `waitTasks` to ensure all async work completes before assertions.
+Tasks are useful not only for SSR but also in tests to wait for async operations inside effects. Pass the signal under test to `waitTasks` to ensure its async work completes before assertions.
```ts
-import { type TasksPool, waitTasks, tasksRunner } from '@nano_kit/store'
-import { client, tasks } from '@nano_kit/query'
+import { expect, vi } from 'vitest'
+import { mountable, signal, computed, onMount, start, task, waitTasks } from '@nano_kit/store'
+
+const fetchName = vi.fn().mockResolvedValue('John')
+const $name = mountable(signal(null))
+const $greeting = computed(() => `Hello, ${$name() ?? 'guest'}`)
+
+/* The store attaches its request to the signal it fills */
+onMount($name, () => {
+ void task($name, async () => {
+ $name(await fetchName())
+ })
+})
+
+/* Mount $greeting and the $name it reads */
+const stop = start($greeting)
-const tasksPool: TasksPool = new Set()
-const { query } = client(tasks(tasksRunner(tasksPool)))
+/* Wait for the tasks of $greeting and of every signal it depends on */
+await waitTasks($greeting)
-/* ... trigger async operations ... */
+expect($greeting()).toBe('Hello, John')
-/* Wait for all tasks to complete */
-await waitTasks(tasksPool)
+stop()
```
-The `waitTasks` function waits for all tasks in the pool to complete, including tasks that spawn new tasks. For simpler cases, use `waitCurrentTasks` to wait only for currently running tasks.
+The `waitTasks` function waits until neither the signal nor anything it depends on has tasks left, including tasks that spawn new tasks. It follows the links the graph already has, so start the signal before waiting: a computed links its dependencies on its first read. Queries, mutations and `resolved` attach their tasks by themselves. `waitTasks` resolves even when a task fails, so assert the error state after it.
## Mocking Dependencies
@@ -1562,3 +1788,53 @@ const context = new InjectionContext([
/* Inject store with mocked context */
const { $episodes } = inject(Episodes$, context)
```
+
+---
+
+# DevTools
+
+Learn how to inspect the stores of a running app with @nano_kit/devtools.
+
+`@nano_kit/devtools` is an in-page DevTools panel for @nano_kit/store: the reactive graph of your app, its values, its lifecycle and a log of every transaction, right on the page.
+
+- **On the page**. A panel over your app in its own shadow root: no browser extension, no bundler plugin, and a strict Content Security Policy does not stop it.
+- **Free in production**. The guarded call folds away with the whole package, and the core reports to the panel in its [development build](#development-builds) alone.
+- **Named for you**. Every signal gets a readable name, the place it was created and its owner, the store factory, the component or the file, straight from the call stack.
+- **Transactions, not events**. The log groups what happened by flush, with the time of every run and the slowest one called out.
+- **Live values**. Pick any node to see its value as a tree, what it reads, what reads it, and its last lines in the log.
+
+## Installation
+
+Install the package as a dev dependency using your favorite package manager:
+
+```bash
+pnpm add -D @nano_kit/devtools
+```
+
+## Quick Start
+
+Call `devtools()` once, behind the development flag of your bundler, and import it before the modules that create your stores, so their signals are named from the start:
+
+```ts title="src/devtools.ts"
+import { devtools } from '@nano_kit/devtools'
+
+if (import.meta.env.DEV) {
+ devtools()
+}
+```
+
+```ts title="src/main.ts"
+/* First of all, before the modules that create stores */
+import './devtools'
+import { App } from './App'
+```
+
+Open the panel with the pill at the bottom edge of the page, or with Alt+Shift+D / ⌥⇧D:
+
+- **Signals**. Every signal, computed and child signal, grouped by the factory, component or file it was created in, with its value, state and links.
+- **Log**. Every transaction: a write and all it caused, down to the effects that ran and how long each took.
+- **Inspector**. The node you picked: its value, what it reads and what reads it, and its recent history.
+
+## Development Builds
+
+`@nano_kit/store`, `@nano_kit/query`, `@nano_kit/router`, `@nano_kit/intl` and the core under them, `kida` and `agera`, ship two builds: `dist/index.development.js` and `dist/index.production.js`. Their `exports` pick one through the `development` and `production` conditions, and `default` is the production build. Bundlers that resolve the `development` condition during development, Vite among them, serve the development build with no setup, while production bundles get the production build.
diff --git a/website/src/content/docs/examples/cvapp.mdx b/website/src/content/docs/examples/cvapp.mdx
index 5089d33a..1d860869 100644
--- a/website/src/content/docs/examples/cvapp.mdx
+++ b/website/src/content/docs/examples/cvapp.mdx
@@ -10,7 +10,7 @@ This example shows a CV editor built with `@nano_kit/store`, `@nano_kit/query`,
It is a compact reference for routed application screens, cached async data, derived state, and React components that subscribe to signals.
± 1.78% | 280.00
± 0.00 | 3441332
± 0.01% | 3571429
± 0 | 3240532 |
-| **@nano_kit/store** | **320.68
± 1.94%** | **290.00
± 10.00** | **3309252
± 0.01%** | **3448276
± 107527** | **3118402** |
-| @preact/signals-core | 338.61
± 1.72% | 310.00
± 0.00 | 3199314
± 0.01% | 3225806
± 0 | 2953273 |
-| svelte/store | 465.27
± 1.90% | 390.00
± 10.00 | 2407386
± 0.02% | 2564103
± 67476 | 2149277 |
-| rxjs | 494.07
± 1.64% | 440.00
± 10.00 | 2171288
± 0.02% | 2272727
± 52854 | 2023990 |
-| zustand, subscribeWithSelector | 949.00
± 11.36% | 540.00
± 10.00 | 1759331
± 0.03% | 1851852
± 34941 | 1061940 |
-| nanostores | 1301.5
± 3.82% | 1010.0
± 30.00 | 922683
± 0.04% | 990099
± 26959 | 768344 |
-| mobx | 3523.9
± 1.97% | 3170.0
± 40.00 | 308802
± 0.03% | 315457
± 3858 | 283777 |
-| valtio | 8159.3
± 42.88% | 4160.0
± 270.00 | 224079
± 0.10% | 240385
± 16186 | 136709 |
-| jotai | 12324
± 18.25% | 6780.0
± 260.00 | 140186
± 0.11% | 147493
± 5760 | 81699 |
-| effector | 26075
± 12.65% | 15950
± 310.00 | 60217
± 0.16% | 62696
± 1495 | 38808 |
-| @reatom/core | 76343
± 18.57% | 50400
± 7410.0 | 19224
± 0.38% | 19841
± 2952 | 13108 |
-
-*Median of 11 rounds, each library measured in a fresh Node.js process in shuffled order. Run on AMD Ryzen 5 PRO 3400G with Node.js v24.14.1.*
+| alien-signals | 300.36
± 0.42% | 290.00
± 0.00 | 3463918
± 0.01% | 3448276
± 0 | 3329289 |
+| **@nano_kit/store** | **322.67
± 1.53%** | **300.00
± 0.00** | **3257518
± 0.01%** | **3333333
± 0** | **3099179** |
+| @preact/signals-core | 337.18
± 1.48% | 310.00
± 0.00 | 3163533
± 0.01% | 3225806
± 0 | 2965757 |
+| svelte/store | 420.22
± 1.75% | 380.00
± 10.00 | 2571799
± 0.01% | 2631579
± 64103 | 2379856 |
+| rxjs | 500.02
± 1.62% | 430.00
± 10.00 | 2208408
± 0.02% | 2325581
± 50505 | 1999907 |
+| zustand, subscribeWithSelector | 978.18
± 9.82% | 530.00
± 10.00 | 1785767
± 0.03% | 1886792
± 36284 | 1025340 |
+| nanostores | 1278.6
± 3.83% | 990.00
± 20.00 | 955623
± 0.03% | 1010101
± 20002 | 782092 |
+| mobx | 2682.5
± 2.55% | 2310.0
± 30.00 | 419932
± 0.03% | 432900
± 5456 | 372782 |
+| valtio | 5777.0
± 29.19% | 3680.0
± 100.00 | 257516
± 0.07% | 271739
± 7590 | 173967 |
+| jotai | 11425
± 9.54% | 7720.0
± 160.00 | 120417
± 0.11% | 129534
± 2727 | 87825 |
+| effector | 25789
± 9.66% | 15940
± 590.50 | 59164
± 0.17% | 62735
± 2366 | 38776 |
+| @reatom/core | 67429
± 12.44% | 44810
± 3630.0 | 20913
± 0.33% | 22316
± 1929 | 14842 |
+
+*Median of 11 rounds, each library measured in a fresh Node.js process in shuffled order. Run on AMD Ryzen 5 PRO 3400G with Node.js v26.10.0.*
## Bundle Sizes
@@ -166,11 +165,11 @@ Comparison of bundle sizes for various implementations of the [weather example](
| Stack | Raw Size | Gzipped Size |
|-------|----------|--------------|
-| React + Nano Stores | 206.77 kB | 65.90 kB |
-| React + **Nano Kit** | **211.32 kB** | **67.25 kB** |
-| React + **Nano Kit + DI** | **212.49 kB** | **67.60 kB** |
-| React + Reatom | 228.16 kB | 73.00 kB |
-| React + TanStack Query | 232.63 kB | 72.42 kB |
+| React + Nano Stores | 206.71 kB | 65.53 kB |
+| React + **Nano Kit** | **211.50 kB** | **66.93 kB** |
+| React + **Nano Kit + DI** | **213.03 kB** | **67.52 kB** |
+| React + Reatom | 228.10 kB | 72.57 kB |
+| React + TanStack Query | 232.56 kB | 71.97 kB |
*Bundled with Vite 8*
@@ -180,9 +179,9 @@ Comparison of **frontend** bundle sizes for various SSR implementations of the [
| Stack | Raw Size | Gzipped Size | Raw Size
`/characters` page |
|-------|----------|--------------|------------------------------|
-| React + **Nano Kit SSR** | **236.62 kB** | **78.01 kB** | **223.87 kB** |
-| React + TanStack Start | 374.95 kB | 117.43 kB | 364.59 kB |
-| Next.js Pages Router + Nano Kit | 509.29 kB | 161.25 kB | 367.60 kB |
-| Next.js App Router + Nano Kit | 946.14 kB | 292.69 kB | 470.21 kB |
+| React + **Nano Kit SSR** | **237.13 kB** | **77.47 kB** | **224.28 kB** |
+| React + TanStack Start | 374.87 kB | 116.02 kB | 364.48 kB |
+| Next.js Pages Router + Nano Kit | 509.58 kB | 160.98 kB | 367.79 kB |
+| Next.js App Router + Nano Kit | 946.42 kB | 291.79 kB | 470.40 kB |
*Bundled with Vite 8 / Next.js 16*
diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx
index 23bab700..279f9f2b 100644
--- a/website/src/content/docs/index.mdx
+++ b/website/src/content/docs/index.mdx
@@ -33,7 +33,7 @@ import { Card, CardGrid } from '@astrojs/starlight/components'
Powered by a fast push-pull reactivity system for fine-grained state updates.
- Complete ecosystem with Router, Query, Dependency Injection, and SSR support.
+ Complete ecosystem with Router, Query, Dependency Injection, SSR support, and DevTools.
Built for type safety with deep type inference across the entire stack.
diff --git a/website/src/content/docs/intl/api.mdx b/website/src/content/docs/intl/api.mdx
index 78a5b440..5b8c49a1 100644
--- a/website/src/content/docs/intl/api.mdx
+++ b/website/src/content/docs/intl/api.mdx
@@ -148,6 +148,20 @@ $t().eventDate(new Date('2024-01-02T00:00:00.000Z'))
This is useful for dates, numbers, and other UI values that come from app state.
+The formatter reads a signal or an accessor passed as the value, so the `$t.eventDate($startsAt)` shortcut is a computed message that follows `$startsAt`. A `null` value formats like a missing one, so data that is still loading needs no guard:
+
+```ts
+const $startsAt = signal(null)
+const $eventDate = $t.eventDate($startsAt)
+
+$eventDate()
+// undefined
+
+$startsAt(new Date('2024-01-02T00:00:00.000Z'))
+$eventDate()
+// 'Jan 2, 2024'
+```
+
## Intl Formats
Intl formats can be used inside `params(...)` for translated templates, or wrapped with `format(...)` when the value comes from runtime state, API data, or a database.
@@ -187,20 +201,35 @@ $t().formatDate(new Date('2024-01-02T00:00:00.000Z'))
### `relativetime(fallback?, options?)`
-Formats relative time values with `Intl.RelativeTimeFormat`.
+Formats the distance from now to a date, a timestamp or a date string with `Intl.RelativeTimeFormat`, in the largest unit the distance fills: years, months, weeks, days, hours, minutes or seconds.
```ts
const [$t] = messages('event', {
formatStartsIn: format(relativetime({
- unit: 'day',
numeric: 'auto'
}))
})
-$t().formatStartsIn(1)
-// 'tomorrow'
+$t().formatStartsIn(event.startsAt)
+// 'tomorrow' for an event that starts in 30 hours
```
+`largestUnit` and `smallestUnit` bound the unit, and both set to one unit format in that unit only. With `numeric: 'auto'`, a distance below the smallest unit reads as "now" when that unit is an hour or less, and as "today" or "this week" for calendar units, so a feed with `smallestUnit: 'minute'` shows "now" for anything newer than a minute:
+
+```ts
+const [$t] = messages('activity', {
+ formatTime: format(relativetime({
+ numeric: 'auto',
+ smallestUnit: 'minute'
+ }))
+})
+
+$t().formatTime(comment.createdAt)
+// 'now', then '5 minutes ago', 'yesterday', 'last week'
+```
+
+`relativeTo` sets the moment to count from instead of the current time. It takes a signal or an accessor as well, so a message follows a ticking clock, for example one built on [`interval()`](/store/advanced/#interval). A month counts as 30 days and a year as 365: the unit comes from the distance, not from the calendar.
+
### `duration(fallback?, options?)`
Formats duration objects with `Intl.DurationFormat`.
diff --git a/website/src/content/docs/intl/core-concepts.mdx b/website/src/content/docs/intl/core-concepts.mdx
index 55ae4b49..60dba0ba 100644
--- a/website/src/content/docs/intl/core-concepts.mdx
+++ b/website/src/content/docs/intl/core-concepts.mdx
@@ -193,7 +193,7 @@ const [$t, $pending, $error] = messages('common', {
})
```
-Use `$t()` to read the whole namespace. Messages are also available as signal properties like `$t.$title`. For parameterized messages, `$t.key(params)` creates a computed signal. If params contain signals, the computed message subscribes to them.
+Use `$t()` to read the whole namespace. Messages are also available as signal properties like `$t.$title`. For parameterized messages, `$t.key(params)` creates a computed signal. If params contain signals, the computed message subscribes to them, and the value of a [`format()`](/intl/api/#formattype) message can be a signal or an accessor itself.
```ts
$t().title
diff --git a/website/src/content/docs/platform/web.mdx b/website/src/content/docs/platform/web.mdx
index aac074ab..8f9c610c 100644
--- a/website/src/content/docs/platform/web.mdx
+++ b/website/src/content/docs/platform/web.mdx
@@ -338,7 +338,7 @@ const context = [
]
```
-`parseLocales` sorts languages by `q` quality and keeps declaration order when qualities are equal. Empty headers fall back to `{ language: 'en', languages: ['en'] }`.
+`parseLocales` sorts languages by `q` quality and keeps declaration order when qualities are equal. An empty or missing header (`null` or `undefined`) falls back to `{ language: 'en', languages: ['en'] }`.
## User Agent
diff --git a/website/src/content/docs/query/advanced.mdx b/website/src/content/docs/query/advanced.mdx
index 41b0caf4..2560acec 100644
--- a/website/src/content/docs/query/advanced.mdx
+++ b/website/src/content/docs/query/advanced.mdx
@@ -101,22 +101,24 @@ You do not need this for ordinary in-memory browser cache usage. While the app i
Use it when serialized cache entries contain values that need custom encoding, such as `Date`, `Map`, `Set`, class instances, or encrypted/compact payloads.
-[`hydratable()`](/query/ssr/#hydratable), [`ssr()`](/query/ssr/#ssr), and [`persistence()`](#persistence) use the codec when they dehydrate data, hydrate it back, write it to persistent storage, or read it back.
+[`hydratable()`](/query/ssr/#hydratable) and [`persistence()`](#persistence) use the codec when they dehydrate data, hydrate it back, write it to persistent storage, or read it back.
```ts
import type { Codec } from '@nano_kit/store'
import { parse, stringify } from 'devalue'
-import { client, codec, ssr } from '@nano_kit/query'
+import { client, codec, hydratable } from '@nano_kit/query'
const DevalueCodec: Codec = {
encode: stringify,
decode: value => value === null ? null : parse(value)
}
-const { query } = client(
- codec(DevalueCodec),
- ssr()
-)
+export function Client$() {
+ return client(
+ codec(DevalueCodec),
+ hydratable()
+ )
+}
```
With a codec like this, data containing values such as `Date`, `Map`, or `Set` can be dehydrated on the server and restored on the client without losing those types.
@@ -187,6 +189,8 @@ keys(invalidate)
This is useful after broad application changes, such as sign in, sign out, locale changes, or any mutation that can affect several query domains at once.
+The name of a builder is its cache shard: two builders with one name share one cache.
+
## Cache Mutation Helpers
For comfortable cache mutations, use helpers that fit naturally into [`$data`](/query/core-concepts#reading-and-writing-data) reducers. They are especially handy for optimistic updates.
@@ -652,28 +656,3 @@ With this setup:
The `capture` helper stores each entity in the shared entity cache and returns a reference for the query result. Passing `entities(PostEntity)` is the shorthand for a single entity result.
**When to use:** Complex applications where the same data (e.g., a "User" or "Product") appears in multiple places or lists and needs to stay synchronized.
-
-### `tasks`
-
-Integrates with `@nano_kit/store`'s task tracking system. This is mainly used for server-side rendering (SSR) to wait for all data fetches to complete before rendering the HTML.
-
-```ts
-import { tasksRunner, waitTasks } from '@nano_kit/store'
-import { client, tasks } from '@nano_kit/query'
-
-const tasksPool = new Set()
-const runTask = tasksRunner(tasksPool)
-
-const { query } = client(
- tasks(runTask)
-)
-
-/* ... application runs ... */
-
-/* Wait for all queries to finish */
-await waitTasks(tasksPool)
-```
-
-Without arguments, `tasks()` reads `TasksRunner$` from the current injection context. Outside DI, pass the runner explicitly.
-
-**When to use:** SSR setups where the server should send a fully populated page to the client.
diff --git a/website/src/content/docs/query/core-concepts.mdx b/website/src/content/docs/query/core-concepts.mdx
index 3913202e..f407a1c9 100644
--- a/website/src/content/docs/query/core-concepts.mdx
+++ b/website/src/content/docs/query/core-concepts.mdx
@@ -272,6 +272,8 @@ SearchKey('react', 2)
This allows different page requests to share the same cache entry.
+The filter is also the place to drop or map a parameter that does not serialize, such as a function or a symbol: it would become `null` in the key, and the development build warns about it.
+
### Using Cache Keys with Client Methods
#### Reading and Writing Data
@@ -332,7 +334,7 @@ revalidate(PostKey)
/* All active post queries will refetch */
```
-**What happens**: Revalidation doesn't remove data from cache, it just marks it as stale. Active queries (those with listeners) will automatically refetch.
+**What happens**: Revalidation doesn't remove data from cache, it just marks it as stale. Active queries (those with listeners) will automatically refetch. If an active query still has a request in flight, the revalidation restarts it, and only the response of the restarted request reaches the cache.
#### Invalidating Cache
diff --git a/website/src/content/docs/query/ssr.mdx b/website/src/content/docs/query/ssr.mdx
index 40038170..e054cf0d 100644
--- a/website/src/content/docs/query/ssr.mdx
+++ b/website/src/content/docs/query/ssr.mdx
@@ -7,53 +7,32 @@ sidebar:
Server-side rendering in `@nano_kit/query` is built around two pieces: task tracking and cache dehydration.
-Queries started during server render should be awaited before HTML is returned, and the resulting cache state should be transferred to the client for hydration.
+Queries started during server render should be awaited before HTML is returned, and the resulting cache state should be transferred to the client for hydration. Task tracking is built in: every query attaches its request as a [task](/store/ssr/#working-with-tasks) to its data signal, so there is nothing to set up for it. Cache dehydration is the [`hydratable()`](#hydratable) setting.
## `hydratable`
-Use `hydratable()` when query cache should be dehydrated on the server and rehydrated on the client.
+Use `hydratable()` when query cache should be dehydrated on the server and rehydrated on the client. It is the whole SSR setting of a query client.
Without arguments, it reads hydration dependencies from the current injection context. Outside DI, pass them explicitly.
-In a manual setup, it is typically paired with `tasks()`, which works the same way: without arguments it reads `TasksRunner$` from the current injection context, and outside DI you can pass the runner explicitly.
-
If the client uses [`codec(...)`](/query/advanced/#codec), `hydratable()` encodes cached data before dehydration and decodes it during hydration.
```ts
-import { client, tasks, hydratable } from '@nano_kit/query'
-
-const { query } = client(
- tasks(),
- hydratable()
-)
-```
-
-Use this when you want to customize task handling or hydration setup independently.
-
-## `ssr`
-
-`ssr()` is a convenience setting that combines `tasks()` and `hydratable()`.
-
-Inside an SSR injection context, it reads the task runner and hydration dependencies automatically. If you need explicit control instead, use `tasks(...)` and `hydratable(...)` directly.
-
-Because `ssr()` uses `hydratable()` internally, it also respects the current [`codec(...)`](/query/advanced/#codec) setting.
-
-```ts
-import { client, dedupeTime, ssr } from '@nano_kit/query'
+import { client, dedupeTime, hydratable } from '@nano_kit/query'
const DEDUPE_TIME = 300_000 // 5 minutes
export function Client$() {
return client(
dedupeTime(DEDUPE_TIME),
- ssr()
+ hydratable()
)
}
```
This enables the usual SSR flow:
-1. Queries started during server render are tracked as tasks.
-2. The renderer waits for those tasks to finish.
+1. Queries started during server render attach their requests as tasks to their data signals.
+2. The renderer waits for the tasks of the signals the page returns from [`Stores$`](/router/ssr#stores-data-dehydration), and of everything they depend on.
3. Query cache state is dehydrated into the hydration payload.
4. The client rehydrates the cache and reuses the prefetched data.
diff --git a/website/src/content/docs/query/testing.mdx b/website/src/content/docs/query/testing.mdx
index a8feb561..e00924be 100644
--- a/website/src/content/docs/query/testing.mdx
+++ b/website/src/content/docs/query/testing.mdx
@@ -17,28 +17,23 @@ Please refer to the [Store Testing](/store/testing) documentation for general gu
## Waiting for Queries
-Tests often need to wait for asynchronous data fetching to resolve. The `tasks` extension provides a reliable way to await all pending operations in your query client, ensuring your assertions run only after data is loaded.
+Tests often need to wait for asynchronous data fetching to resolve. Every query attaches its request as a task to its data signal, so [`waitTasks`](/store/testing#waiting-for-async-operations) of that signal awaits the request with no extra setup, ensuring your assertions run only after data is loaded.
```ts
import { describe, it, expect, vi } from 'vitest'
-import { tasksRunner, waitTasks, start } from '@nano_kit/store'
-import { client, tasks, queryKey } from '@nano_kit/query'
+import { waitTasks, start } from '@nano_kit/store'
+import { client, queryKey } from '@nano_kit/query'
const TestKey = queryKey('test')
it('should fetch and update state', async () => {
- /* 1. Create a pool to track active tasks */
- const tasksPool = new Set()
-
- /* 2. Configure client with tasks extension */
- const { query } = client(
- tasks(tasksRunner(tasksPool))
- )
+ /* 1. Create a client, no task setup is needed */
+ const { query } = client()
/* Mock fetcher */
const fetcher = vi.fn().mockResolvedValue('success')
- /* 3. Start the query */
+ /* 2. Start the query */
const [$data, $error, $loading] = query(TestKey, [], fetcher)
/* Mount the signal to trigger the fetch */
@@ -46,13 +41,15 @@ it('should fetch and update state', async () => {
expect($loading()).toBe(true)
- /* 4. Wait for all fetch tasks to complete */
- await waitTasks(tasksPool)
+ /* 3. Wait for the request of $data to complete */
+ await waitTasks($data)
- /* 5. Assert final state */
+ /* 4. Assert final state */
expect($loading()).toBe(false)
expect($data()).toBe('success')
stop()
})
```
+
+`waitTasks` of any signal computed from `$data` waits for the request as well. Infinite queries and operations attach their requests to `$data` too. A mutation attaches each request to all of its signals, so waiting for any of them works, as does awaiting the promise returned by `mutate`.
diff --git a/website/src/content/docs/router/advanced.mdx b/website/src/content/docs/router/advanced.mdx
index 2baa00ed..3ff62b84 100644
--- a/website/src/content/docs/router/advanced.mdx
+++ b/website/src/content/docs/router/advanced.mdx
@@ -128,6 +128,18 @@ export function Head$() {
}
```
+### `toHtmlAttribute`
+
+Descriptor props are camelCase, like `httpEquiv` or `crossOrigin`. `toHtmlAttribute` maps a prop name to its HTML attribute name, the same way `syncHead` and the SSR renderer do. Use it when a custom adapter turns descriptor props into attributes itself.
+
+```ts
+import { toHtmlAttribute } from '@nano_kit/router'
+
+toHtmlAttribute('httpEquiv') // 'http-equiv'
+toHtmlAttribute('crossOrigin') // 'crossorigin'
+toHtmlAttribute('content') // 'content'
+```
+
## Scroll Management
Since scroll restoration and behavior can vary significantly between applications, the library provides a set of "Do It Yourself" (DIY) utilities instead of a one-size-fits-all solution. You can combine these tools to implement the exact scrolling behavior your app needs.
diff --git a/website/src/content/docs/ssr/index.mdx b/website/src/content/docs/ssr/index.mdx
index 181d799a..0578b5e9 100644
--- a/website/src/content/docs/ssr/index.mdx
+++ b/website/src/content/docs/ssr/index.mdx
@@ -68,7 +68,7 @@ An adapter consists of three parts: a **renderer**, a **client entry**, and a **
```js
// src/renderer/index.js
- import { Renderer, headDescriptorToHtml, ROOT_ID } from '@nano_kit/ssr/renderer'
+ import { Renderer, escapeHtml, headDescriptorToHtml, ROOT_ID } from '@nano_kit/ssr/renderer'
import { compose } from 'your-framework-router'
import { get } from '@nano_kit/store'
@@ -87,15 +87,17 @@ An adapter consists of three parts: a **renderer**, a **client entry**, and a **
else head += headDescriptorToHtml(descriptor)
})
- if (title) head = `${title}${head}`
+ if (title) head = `${escapeHtml(title)}${head}`
const body = yourFrameworkRenderToString(data.context)
- return `${head}${body}
`
+ return `${head}${body}
`
}
}
```
+ `headDescriptorToHtml` escapes the attribute values of the tags it renders and `dehydratedScript` escapes the snapshot, so `escapeHtml` is needed only for the values the renderer interpolates itself: `title`, `lang` and `dir`.
+
Also provide a **default renderer template** that the Vite plugin uses when the user does not supply a custom one:
```js
diff --git a/website/src/content/docs/ssr/locale.mdx b/website/src/content/docs/ssr/locale.mdx
index d62f725c..7f3d4a0b 100644
--- a/website/src/content/docs/ssr/locale.mdx
+++ b/website/src/content/docs/ssr/locale.mdx
@@ -127,4 +127,4 @@ browserLocale(context.get(Locales$), ['en', 'ru-RU'], 'en')
// 'ru-RU'
```
-`parseLocales` sorts languages by `q` quality and keeps declaration order when qualities are equal. Empty headers fall back to `{ language: 'en', languages: ['en'] }`.
+`parseLocales` sorts languages by `q` quality and keeps declaration order when qualities are equal. An empty or missing header (`null` or `undefined`) falls back to `{ language: 'en', languages: ['en'] }`.
diff --git a/website/src/content/docs/store-integrations/preact.mdx b/website/src/content/docs/store-integrations/preact.mdx
index 6b38e75c..21dec1f2 100644
--- a/website/src/content/docs/store-integrations/preact.mdx
+++ b/website/src/content/docs/store-integrations/preact.mdx
@@ -57,6 +57,8 @@ To use the Dependency Injection system within Preact, wrap your application (or
You can provide an existing context instance or an array of providers/values.
+Inside another provider, one with an array of providers creates a child context, and one without `context` reuses the parent. Which context a store is created in is explained in [Nested Contexts](/store/core-concepts#nested-contexts).
+
```tsx
import { provide } from '@nano_kit/store'
import { InjectionContextProvider } from '@nano_kit/preact'
diff --git a/website/src/content/docs/store-integrations/react.mdx b/website/src/content/docs/store-integrations/react.mdx
index bc08eba7..c039e3cd 100644
--- a/website/src/content/docs/store-integrations/react.mdx
+++ b/website/src/content/docs/store-integrations/react.mdx
@@ -57,6 +57,8 @@ To use the Dependency Injection system within React, wrap your application (or p
You can provide an existing context instance or an array of providers/values.
+Inside another provider, one with an array of providers creates a child context, and one without `context` reuses the parent. Which context a store is created in is explained in [Nested Contexts](/store/core-concepts#nested-contexts).
+
```tsx
import { provide } from '@nano_kit/store'
import { InjectionContextProvider } from '@nano_kit/react'
@@ -164,7 +166,7 @@ import { User$ } from './stores'
function Stores$() {
const { $user } = inject(User$)
- /* Will execute store logic, wait for all async tasks to complete, and extract the dehydrated snapshot */
+ /* Will execute store logic, wait for the async tasks of these stores to complete, and extract the dehydrated snapshot */
return [$user]
}
@@ -191,7 +193,7 @@ import { User$ } from './stores'
function Stores$() {
const { $user } = inject(User$)
- /* Will execute store logic, wait for all async tasks to complete, and extract the dehydrated snapshot */
+ /* Will execute store logic, wait for the async tasks of these stores to complete, and extract the dehydrated snapshot */
return [$user]
}
@@ -229,7 +231,7 @@ import { User$ } from './stores'
function Stores$() {
const { $user } = inject(User$)
- /* Will execute store logic, wait for all async tasks to complete, and extract the dehydrated snapshot */
+ /* Will execute store logic, wait for the async tasks of these stores to complete, and extract the dehydrated snapshot */
return [$user]
}
diff --git a/website/src/content/docs/store-integrations/svelte.mdx b/website/src/content/docs/store-integrations/svelte.mdx
index 202ffdbe..856edf8d 100644
--- a/website/src/content/docs/store-integrations/svelte.mdx
+++ b/website/src/content/docs/store-integrations/svelte.mdx
@@ -4,6 +4,8 @@ description: "Integrate @nano_kit/store with Svelte using @nano_kit/svelte."
sidebar:
label: Svelte
order: 3
+next:
+ label: Query
---
import { Tabs, TabItem, Code, Aside } from '@astrojs/starlight/components'
@@ -53,6 +55,8 @@ To use the Dependency Injection system within Svelte, initialize an injection co
You can provide an existing instance or an array of providers/values.
+Called with providers under another context, `setInjectionContext` creates a child context of it. Which context a store is created in is explained in [Nested Contexts](/store/core-concepts#nested-contexts).
+
```svelte