nanostores
Advanced tools
Comparing version 0.3.3 to 0.4.0
@@ -21,3 +21,3 @@ import { MapBuilder, AnySyncBuilder } from '../define-map/index.js' | ||
export function cleanStores( | ||
...stores: (ReadableStore | MapBuilder | AnySyncBuilder)[] | ||
...stores: (ReadableStore | MapBuilder | AnySyncBuilder | undefined)[] | ||
): void |
@@ -10,5 +10,7 @@ export const clean = Symbol('clean') | ||
for (let store of stores) { | ||
if (store.mocked) delete store.mocked | ||
store[clean]() | ||
if (store) { | ||
if (store.mocked) delete store.mocked | ||
store[clean]() | ||
} | ||
} | ||
} |
import { clean } from '../clean-stores/index.js' | ||
export function createMap(init) { | ||
let listeners = [] | ||
let currentListeners | ||
let nextListeners = [] | ||
let destroy | ||
@@ -37,3 +38,4 @@ | ||
notify(changedKey) { | ||
for (let listener of listeners) { | ||
currentListeners = nextListeners | ||
for (let listener of currentListeners) { | ||
listener(store.value, changedKey) | ||
@@ -55,9 +57,15 @@ } | ||
} | ||
listeners.push(listener) | ||
if (nextListeners === currentListeners) { | ||
nextListeners = nextListeners.slice() | ||
} | ||
nextListeners.push(listener) | ||
return () => { | ||
let index = listeners.indexOf(listener) | ||
listeners.splice(index, 1) | ||
if (!listeners.length) { | ||
if (nextListeners === currentListeners) { | ||
nextListeners = nextListeners.slice() | ||
} | ||
let index = nextListeners.indexOf(listener) | ||
nextListeners.splice(index, 1) | ||
if (!nextListeners.length) { | ||
setTimeout(() => { | ||
if (store.active && !listeners.length) { | ||
if (store.active && !nextListeners.length) { | ||
if (destroy) destroy() | ||
@@ -78,3 +86,3 @@ store.active = false | ||
store.value = undefined | ||
listeners = [] | ||
nextListeners = [] | ||
destroy = undefined | ||
@@ -81,0 +89,0 @@ } |
import { clean } from '../clean-stores/index.js' | ||
export function createStore(init) { | ||
let listeners = [] | ||
let currentListeners | ||
let nextListeners = [] | ||
let destroy | ||
@@ -10,3 +11,4 @@ | ||
store.value = newValue | ||
for (let listener of listeners) { | ||
currentListeners = nextListeners | ||
for (let listener of currentListeners) { | ||
listener(store.value) | ||
@@ -27,9 +29,15 @@ } | ||
} | ||
listeners.push(listener) | ||
if (nextListeners === currentListeners) { | ||
nextListeners = nextListeners.slice() | ||
} | ||
nextListeners.push(listener) | ||
return () => { | ||
let index = listeners.indexOf(listener) | ||
listeners.splice(index, 1) | ||
if (!listeners.length) { | ||
if (nextListeners === currentListeners) { | ||
nextListeners = nextListeners.slice() | ||
} | ||
let index = nextListeners.indexOf(listener) | ||
nextListeners.splice(index, 1) | ||
if (!nextListeners.length) { | ||
setTimeout(() => { | ||
if (store.active && !listeners.length) { | ||
if (store.active && !nextListeners.length) { | ||
if (destroy) destroy() | ||
@@ -50,3 +58,3 @@ destroy = undefined | ||
store.value = undefined | ||
listeners = [] | ||
nextListeners = [] | ||
destroy = undefined | ||
@@ -53,0 +61,0 @@ } |
export { | ||
createRouter, | ||
RouteParams, | ||
getPagePath, | ||
openPage, | ||
redirectPage, | ||
Router, | ||
Page | ||
} from './create-router/index.js' | ||
export { | ||
BuilderValue, | ||
@@ -22,7 +13,8 @@ BuilderStore, | ||
} from './create-store/index.js' | ||
export { startEffect, effect, allEffects } from './effect/index.js' | ||
export { createMap, MapStore } from './create-map/index.js' | ||
export { clean, cleanStores } from './clean-stores/index.js' | ||
export { createPersistent } from './create-persistent/index.js' | ||
export { update, updateKey } from './update/index.js' | ||
export { createDerived } from './create-derived/index.js' | ||
export { keepActive } from './keep-active/index.js' | ||
export { getValue } from './get-value/index.js' |
@@ -1,9 +0,4 @@ | ||
export { | ||
createRouter, | ||
openPage, | ||
redirectPage, | ||
getPagePath | ||
} from './create-router/index.js' | ||
export { startEffect, effect, allEffects } from './effect/index.js' | ||
export { clean, cleanStores } from './clean-stores/index.js' | ||
export { createPersistent } from './create-persistent/index.js' | ||
export { update, updateKey } from './update/index.js' | ||
export { createDerived } from './create-derived/index.js' | ||
@@ -10,0 +5,0 @@ export { createStore } from './create-store/index.js' |
{ | ||
"name": "nanostores", | ||
"version": "0.3.3", | ||
"description": "A tiny (152 bytes) state manager for React/Preact/Vue/Svelte with many atomic tree-shakable stores", | ||
"version": "0.4.0", | ||
"description": "A tiny (172 bytes) state manager for React/Preact/Vue/Svelte with many atomic tree-shakable stores", | ||
"keywords": [ | ||
"logux", | ||
"nano", | ||
"state", | ||
"state manager", | ||
"react", | ||
"react native", | ||
"preact", | ||
@@ -16,4 +18,3 @@ "vue", | ||
"license": "MIT", | ||
"homepage": "https://logux.io/", | ||
"repository": "ai/nanostores", | ||
"repository": "nanostores/nanostores", | ||
"sideEffects": false, | ||
@@ -29,2 +30,5 @@ "type": "module", | ||
}, | ||
"react-native": { | ||
"./react/batch/index.js": "./react/batch/index.native.js" | ||
}, | ||
"engines": { | ||
@@ -31,0 +35,0 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" |
@@ -1,6 +0,8 @@ | ||
import { unstable_batchedUpdates } from 'react-dom' | ||
import React from 'react' | ||
import { getValue } from '../get-value/index.js' | ||
import { batch } from './batch/index.js' | ||
export { batch } | ||
export function useStore(store) { | ||
@@ -20,3 +22,3 @@ let [, forceRender] = React.useState({}) | ||
let unbind = store.listen(() => { | ||
unstable_batchedUpdates(() => { | ||
batch(() => { | ||
forceRender({}) | ||
@@ -23,0 +25,0 @@ }) |
394
README.md
# Nano Stores | ||
<img align="right" width="95" height="148" title="Logux logotype" | ||
src="https://logux.io/branding/logotype.svg"> | ||
<img align="right" width="92" height="92" title="Nano Stores logo" | ||
src="https://nanostores.github.io/nanostores/logo.svg"> | ||
A tiny state manager for **React**, **Preact**, **Vue** and **Svelte**. | ||
It uses **many atomic stores** and direct manipulation. | ||
A tiny state manager for **React**, **React Native**, **Preact**, **Vue**, | ||
**Svelte**, and vanilla JS. It uses **many atomic stores** | ||
and direct manipulation. | ||
* **Small.** 152 bytes (minified and gzipped). Zero dependencies. | ||
It uses [Size Limit] to control size. | ||
* **Small.** between 172 and 527 bytes (minified and gzipped). | ||
Zero dependencies. It uses [Size Limit] to control size. | ||
* **Fast.** With small atomic and derived stores, you do not need to call | ||
the selector function for all components on every store change. | ||
the selector function for all components on every store change. | ||
* **Tree Shakable.** The chunk contains only stores used by components | ||
in the chunk. | ||
* Was designed to move logic from components to stores. Already has **router** | ||
and **persistent** stores. | ||
* Was designed to move logic from components to stores. | ||
* It has good **TypeScript** support. | ||
@@ -21,3 +21,3 @@ | ||
// store/users.ts | ||
import { createStore, getValue } from 'nanostores' | ||
import { createStore, update } from 'nanostores' | ||
@@ -29,3 +29,3 @@ export const users = createStore<User[]>(() => { | ||
export function addUser(user: User) { | ||
users.set([...getValue(users), user]) | ||
update(users, current => [...current, user]) | ||
} | ||
@@ -61,6 +61,3 @@ ``` | ||
It is part of [Logux] project but can be used without any other Logux parts. | ||
<a href="https://evilmartians.com/?utm_source=logux-client"> | ||
<a href="https://evilmartians.com/?utm_source=nanostores"> | ||
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" | ||
@@ -70,6 +67,19 @@ alt="Sponsored by Evil Martians" width="236" height="54"> | ||
[Size Limit]: https://github.com/ai/size-limit | ||
[Logux]: https://logux.io/ | ||
[Size Limit]: https://github.com/ai/size-limit | ||
## Table of Contents | ||
* [Tools](#tools) | ||
* [Guide](#guide) | ||
* Integration | ||
* [React & Preact](#react--preact) | ||
* [Next.js](#nextjs) | ||
* [Vue](#vue) | ||
* [Svelte](#svelte) | ||
* [Vanilla JS](#vanilla-js) | ||
* [Tests](#tests) | ||
* [Best Practices](#best-practices) | ||
* [Known Issues](#known-issues) | ||
## Install | ||
@@ -83,4 +93,5 @@ | ||
* [Persistent](#persistent) store to save data to `localStorage`. | ||
* [Router](#router) store. | ||
* [Persistent](https://github.com/nanostores/persistent) store to save data | ||
to `localStorage` and synchronize changes between browser tabs. | ||
* [Router](https://github.com/nanostores/router) store. | ||
* [Logux Client](https://github.com/logux/client): stores with WebSocket | ||
@@ -90,8 +101,8 @@ sync and CRDT conflict resolution. | ||
## Stores | ||
## Guide | ||
In Nano Stores, stores are **smart**. They subscribe to events, | ||
validate input, send AJAX requests, etc. For instance, | ||
build-in [Router](#Router) store subscribes to click on `<a>` | ||
and `window.onpopstate`. It simplifies testing and switching | ||
[Router](https://github.com/nanostores/router) store subscribes to click | ||
on `<a>` and `window.onpopstate`. It simplifies testing and switching | ||
between UI frameworks (like from React to React Native). | ||
@@ -131,3 +142,3 @@ | ||
By we have shortcut to subscribe, return value and unsubscribe: | ||
We have shortcut to subscribe, return value and unsubscribe: | ||
@@ -140,3 +151,11 @@ ```ts | ||
And there is shortcut to get current value, change it and set new value. | ||
```ts | ||
import { update } from 'nanostores' | ||
update(store, value => newValue) | ||
``` | ||
### Simple Store | ||
@@ -147,3 +166,3 @@ | ||
```ts | ||
import { createStore, getValue } from 'nanostores' | ||
import { createStore, update } from 'nanostores' | ||
@@ -155,3 +174,3 @@ export const counter = createStore<number>(() => { | ||
export function increaseCounter() { | ||
counter.set(getValue(counter) + 1) | ||
update(counter, value => value + 1) | ||
} | ||
@@ -162,6 +181,19 @@ ``` | ||
All async operations in store you need to wrap to `effect` (or use `startEffect`). | ||
It will help to wait async operations end in tests. | ||
```ts | ||
import { effect } from 'nanostore' | ||
export function saveUser() { | ||
effect(async () => { | ||
await api.saveUser(getValue(userStore)) | ||
}) | ||
} | ||
``` | ||
### Map Store | ||
This store with key-value pairs. | ||
This store is with key-value pairs. | ||
@@ -182,3 +214,4 @@ ```ts | ||
In additional to `store.set(newObject)` it has `store.setKey(key, value)` | ||
to change specific key. | ||
to change specific key. There is a special shortcut | ||
`updateKey(store, key, updater)` in additional to `update(store, updater)`. | ||
@@ -227,3 +260,3 @@ Changes listener receives changed key as a second argument. | ||
A template to create a similar store. Each store made by the template | ||
is map store with at least the `id` key. | ||
is a map store with at least the `id` key. | ||
@@ -270,80 +303,2 @@ ```ts | ||
## Best Practices | ||
### Move Logic from Components to Stores | ||
Stores are not only to keep values. You can use them to track time, to load data | ||
from server. | ||
```ts | ||
import { createStore } from 'nanostores' | ||
export const currentTime = createStore<number>(() => { | ||
currentTime.set(Date.now()) | ||
const updating = setInterval(() => { | ||
currentTime.set(Date.now()) | ||
}, 1000) | ||
return () => { | ||
clearInterval(updating) | ||
} | ||
}) | ||
``` | ||
Use derived stores to create chains of reactive computations. | ||
```ts | ||
import { createDerived } from 'nanostores' | ||
import { currentTime } from './currentTime.js' | ||
const appStarted = Date.now() | ||
export const userInApp = createDerived(currentTime, now => { | ||
return now - appStarted | ||
}) | ||
``` | ||
We recommend moving all logic, which is not highly related to UI to the stores. | ||
Let your stores track URL routing, validation, sending data to a server. | ||
With application logic in the stores, it’s much easy to write and run tests. | ||
It is also easy to change your UI framework. For instance, add React Native | ||
version of the application. | ||
### Think about Tree Shaking | ||
We recommend doing all store changes in separated functions. It will allow | ||
to tree shake unused functions from JS bundle. | ||
```ts | ||
export function changeStore (newValue: string) { | ||
if (validate(newValue)) { | ||
throw new Error('New value is not valid') | ||
} else { | ||
store.set(newValue) | ||
} | ||
} | ||
``` | ||
For builder, you can add properties to the store, but try to avoid it. | ||
```ts | ||
interface UserExt { | ||
avatarCache?: string | ||
} | ||
export function User = defineMap<UserValue, [], UserExt>((store, id) => { | ||
… | ||
}) | ||
function getAvatar (user: BuilderStore<typeof User>) { | ||
if (!user.avatarCache) { | ||
user.avatarCache = generateAvatar(getValue(user).email) | ||
} | ||
return user.avatarCache | ||
} | ||
``` | ||
## Integration | ||
@@ -363,8 +318,9 @@ | ||
export const Header = () => { | ||
const profile = useStore(profile) | ||
const currentUser = useStore(User(profile.userId)) | ||
return <header>${currentUser.name}<header> | ||
const { userId } = useStore(profile) | ||
const currentUser = useStore(User(userId)) | ||
return <header>{currentUser.name}<header> | ||
} | ||
``` | ||
### Vue | ||
@@ -388,4 +344,4 @@ | ||
setup () { | ||
const profile = useStore(profile) | ||
const currentUser = useStore(User(profile.value.userId)) | ||
const { userId } = useStore(profile).value | ||
const currentUser = useStore(User(userId)) | ||
return { currentUser } | ||
@@ -397,2 +353,3 @@ } | ||
### Svelte | ||
@@ -410,4 +367,4 @@ | ||
const profile = useStore(profile) | ||
const currentUser = useStore(User(profile.userId)) | ||
const { userId } = useStore(profile) | ||
const currentUser = useStore(User(userId)) | ||
</script> | ||
@@ -419,2 +376,26 @@ | ||
### Vanilla JS | ||
`Store#subscribe()` calls callback immediately and subscribes to store changes. | ||
It passes store’s value to callback. | ||
```js | ||
let prevUserUnbind | ||
profile.subscribe(({ userId }) => { | ||
// Re-subscribe on current user ID changes | ||
if (prevUserUnbind) { | ||
// Remove old user listener | ||
prevUserUnbind() | ||
} | ||
// Add new user listener | ||
prevUserUnbind = User(userId).subscribe(currentUser => { | ||
console.log(currentUser.name) | ||
}) | ||
}) | ||
``` | ||
Use `Store#listen()` if you need to add listener without calling | ||
callback immediately. | ||
### Tests | ||
@@ -441,72 +422,175 @@ | ||
You can use `allEffects()` to wait all async options in stores. | ||
## Build-in Stores | ||
```ts | ||
import { getValue, allEffects } from 'nanostores' | ||
### Persistent | ||
it('saves user', async () => { | ||
saveUser() | ||
await allEffects() | ||
expect(getValue(analyticsEvents)).toEqual(['user:save']) | ||
}) | ||
``` | ||
You can create a store to keep value with some prefix in `localStorage`. | ||
## Best Practices | ||
### Move Logic from Components to Stores | ||
Stores are not only to keep values. You can use them to track time, to load data | ||
from server. | ||
```ts | ||
import { createPersistent } from 'nanostores' | ||
import { createStore } from 'nanostores' | ||
export interface CartValue { | ||
list: string[] | ||
} | ||
export const currentTime = createStore<number>(() => { | ||
currentTime.set(Date.now()) | ||
const updating = setInterval(() => { | ||
currentTime.set(Date.now()) | ||
}, 1000) | ||
return () => { | ||
clearInterval(updating) | ||
} | ||
}) | ||
``` | ||
export const shoppingCart = createPersistent<CartValue>({ list: [] }, 'cart') | ||
Use derived stores to create chains of reactive computations. | ||
```ts | ||
import { createDerived } from 'nanostores' | ||
import { currentTime } from './currentTime.js' | ||
const appStarted = Date.now() | ||
export const userInApp = createDerived(currentTime, now => { | ||
return now - appStarted | ||
}) | ||
``` | ||
This store also listen for keys changes in `localStorage` and can be used | ||
to synchronize changes between browser tabs. | ||
We recommend moving all logic, which is not highly related to UI, to the stores. | ||
Let your stores track URL routing, validation, sending data to a server. | ||
With application logic in the stores, it is much easier to write and run tests. | ||
It is also easy to change your UI framework. For instance, add React Native | ||
version of the application. | ||
### Router | ||
Since we promote moving logic to store, the router is a good part | ||
of the application to be moved from UI framework like React. | ||
### Think about Tree Shaking | ||
We recommend doing all store changes in separated functions. It will allow | ||
to tree shake unused functions from JS bundle. | ||
```ts | ||
import { createRouter } from 'nanostores' | ||
export function changeStore (newValue: string) { | ||
if (validate(newValue)) { | ||
throw new Error('New value is not valid') | ||
} else { | ||
store.set(newValue) | ||
} | ||
} | ||
``` | ||
// Types for :params in route templates | ||
interface Routes { | ||
home: void | ||
category: 'categoryId' | ||
post: 'categoryId' | 'id' | ||
For builder, you can add properties to the store, but try to avoid it. | ||
```ts | ||
interface UserExt { | ||
avatarCache?: string | ||
} | ||
export const router = createRouter<Routes>({ | ||
home: '/', | ||
category: '/posts/:categoryId', | ||
post: '/posts/:categoryId/:id' | ||
export function User = defineMap<UserValue, [], UserExt>((store, id) => { | ||
… | ||
}) | ||
function getAvatar (user: BuilderStore<typeof User>) { | ||
if (!user.avatarCache) { | ||
user.avatarCache = generateAvatar(getValue(user).email) | ||
} | ||
return user.avatarCache | ||
} | ||
``` | ||
Store in active mode listen for `<a>` clicks on `document.body` and Back button | ||
in browser. | ||
### Separate changes and reaction | ||
You can use `getPagePath()` to avoid hard coding URL to a template. It is better | ||
to use the router as a single place of truth. | ||
Use a separated listener to react on new store’s value, not a function where you | ||
change this store. | ||
```tsx | ||
import { getPagePath } from 'nanostores' | ||
```diff | ||
function increase () { | ||
update(counter, value => value + 1) | ||
- printCounter(getValue(counter)) | ||
} | ||
… | ||
<a href={getPagePath(router, 'post', { categoryId: 'guides', id: '10' })}> | ||
+ counter.subscribe(value => { | ||
+ printCounter(value) | ||
+ }) | ||
``` | ||
If you need to change URL programmatically you can use `openPage` | ||
or `replacePage`: | ||
A "change" function is not only a way for store to a get new value. | ||
For instance, persistent store could get the new value from another browser tab. | ||
```ts | ||
import { openPage, replacePage } from 'nanostores' | ||
With this separation your UI will be ready to any source of store’s changes. | ||
function requireLogin () { | ||
openPage(router, 'login') | ||
} | ||
function onLoginSuccess() { | ||
// Replace login route, so we don’t face it on back navigation | ||
replacePage(router, 'home') | ||
} | ||
### Reduce `getValue()` usage outside of tests | ||
`getValue()` returns current value and it is a good solution for tests. | ||
But it is better to use `useStore()`, `$store`, or `Store#subscribe()` in UI | ||
to subscribe to store changes and always render the actual data. | ||
```diff | ||
- const { userId } = getValue(profile) | ||
+ const { userId } = useStore(profile) | ||
``` | ||
In store’s functions you can use `update` and `updateKey` shortcuts: | ||
```diff | ||
function increase () { | ||
- counter.set(getValue(counter) + 1) | ||
+ update(counter, value => value + 1) | ||
} | ||
``` | ||
## Known Issues | ||
### Diamond Problem | ||
To make stores simple and small, Nano Stores doesn’t solve “Diamond problem”. | ||
``` | ||
A | ||
↓ | ||
F←B→C | ||
↓ ↓ | ||
↓ D | ||
↓ ↓ | ||
G→H←E | ||
``` | ||
On `A` store changes, `H` store will be called twice in different time | ||
by change signals coming from different branches. | ||
You need to care about these changes on your own. | ||
### ESM | ||
Nano Stores use ES modules and doesn’t provide CommonJS exports. | ||
You need to use ES modules in your application to import Nano Stores. | ||
For instance, for Next.js you need to use [`next-transpile-modules`] to fix | ||
lack of ESM support in Next.js. | ||
```js | ||
// next.config.js | ||
const withTM = require('next-transpile-modules')(['nanostores']) | ||
module.exports = withTM({ | ||
/* previous configuration goes here */ | ||
}) | ||
``` | ||
[`next-transpile-modules`]: https://www.npmjs.com/package/next-transpile-modules |
@@ -5,4 +5,2 @@ import { DeepReadonly, Ref } from 'vue' | ||
type ReadonlyRef<Type> = DeepReadonly<Ref<Type>> | ||
/** | ||
@@ -22,5 +20,7 @@ * Subscribe to store changes and get store’s value. | ||
* | ||
* export default () => { | ||
* let page = useStore(router) | ||
* return { page } | ||
* export default { | ||
* setup () { | ||
* let page = useStore(router) | ||
* return { page } | ||
* } | ||
* } | ||
@@ -35,2 +35,2 @@ * </script> | ||
store: ReadableStore<Value> | ||
): ReadonlyRef<Value> | ||
): DeepReadonly<Ref<Type>> |
@@ -10,4 +10,3 @@ import { | ||
export function useStore(store) { | ||
let state = ref(null) | ||
let readonlyState = readonly(state) | ||
let state = ref() | ||
let unsubscribe | ||
@@ -31,3 +30,6 @@ | ||
return readonlyState | ||
if (process.env.NODE_ENV !== 'production') { | ||
return readonly(state) | ||
} | ||
return state | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
33
577
8
38877
888
2