Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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] |
Expand Down Expand Up @@ -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 -->

[devtools-npm]: https://img.shields.io/npm/v/%40nano_kit%2Fdevtools.svg
[devtools-npm-url]: https://npmjs.com/package/@nano_kit/devtools

<!-- react -->

[react-npm]: https://img.shields.io/npm/v/%40nano_kit%2Freact.svg
Expand Down
18 changes: 9 additions & 9 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 🪨 |
6 changes: 2 additions & 4 deletions packages/agera/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -276,8 +276,6 @@ effect(() => {
untracked(() => {
const b = $b()

resumeTracking()

console.log('Sum:', a + b)
})
})
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
105 changes: 98 additions & 7 deletions packages/kida/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -404,7 +404,7 @@ const $user = atKey($users, $id)

console.log($user()) // John

$index(6)
$id(6)

console.log($user()) // Alice
```
Expand All @@ -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.
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -626,6 +717,6 @@ Nano Stores is a great library with wonderful idea of stores with lifecycles. Bu

| Benchmark<br>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% |
6 changes: 3 additions & 3 deletions packages/ssr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -64,12 +64,12 @@ export class FrameworkRenderer extends Renderer {
else head += headDescriptorToHtml(descriptor)
})

if (title) head = `<title>${title}</title>${head}`
if (title) head = `<title>${escapeHtml(title)}</title>${head}`

// render to string using your framework, wrapping with data.context
const body = yourFrameworkRenderToString(data.context)

return `<html lang="${lang}" dir="${dir}"><head>${head}</head><body><div id="${ROOT_ID}">${body}</div><script>${this.dehydratedScript(data.dehydrated)}</script></body></html>`
return `<html${lang ? ` lang="${escapeHtml(lang)}"` : ''}${dir ? ` dir="${escapeHtml(dir)}"` : ''}><head>${head}</head><body><div id="${ROOT_ID}">${body}</div><script>${this.dehydratedScript(data.dehydrated)}</script></body></html>`
}
}
```
Expand Down
7 changes: 4 additions & 3 deletions packages/store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, () => {
Expand All @@ -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)
Expand Down
39 changes: 34 additions & 5 deletions skills/nano-kit-intl/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Date | null>(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.
Expand Down Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion skills/nano-kit-platform-web/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading