跳到主要内容

guide

创建日期:2023-11-02 | 最近更新:2024-01-02

用于 React、React Native、Preact、Vue、 Svelte、Solid、Lit、Angular 和 vanilla JS 的小型状态管理器。使用原子存储和直接操作。

// store/users.ts
import { atom } from 'nanostores';

export const $users = atom<User[]>([]);

export function addUser(user: User) {
$users.set([...$users.get(), user]);
}
// store/admins.ts
import { computed } from 'nanostores';
import { $users } from './users.js';

export const $admins = computed($users, (users) => users.filter((i) => i.isAdmin));
// components/admins.tsx
import { useStore } from '@nanostores/react';
import { $admins } from '../stores/admins.js';

export const Admins = () => {
const admins = useStore($admins);
return (
<ul>
{admins.map((user) => (
<UserItem user={user} />
))}
</ul>
);
};

Atoms

Atom store 被用来存储 string,number,array

你也可以用在 object 上,如果你只是替换整个对象而不是修改它的属性。

创建一个 atom 需要给一个初始值

import { atom } from 'nanostores';
export const $counter = atom(0);

store.get()获取值. store.set(nextValue) 改变值并触发更新

$counter.set($counter.get() + 1);

store.subscribe(cb) and store.listen(cb) can be used to subscribe for the changes in vanilla JS. For React/Vue we have extra special helpers useStore to re-render the component on any store changes.

const unbindListener = $counter.subscribe(value => { console.log('counter value:', value) })

store.subscribe(cb) in contrast with store.listen(cb) also call listeners immediately during the subscription.

Maps

Map store 可以用来存储层级为一级的对象

import { map } from 'nanostores';

export const $profile = map({
name: 'anonymous',
});

使用 store.set(object) 或者 store.setKey(key, value)来改变值

$profile.setKey('name', 'Kazimir Malevich');

Setting undefined will remove optional key:

$profile.setKey('email', undefined);

监听器会接收第二个参数,这个参数是监听到变化的 key

$profile.listen((profile, changed) => {
console.log(`${changed} new value ${profile[changed]}`);
});

Deep maps

Deep maps 与map类似,但是可以存储层级为多级的对象

import { deepMap, listenKeys } from 'nanostores';

export const $profile = deepMap({
hobbies: [
{
name: 'woodworking',
friends: [{ id: 123, name: 'Ron Swanson' }],
},
],
});

listenKeys($profile, ['hobbies[0].friends[0].name']);

// Won't fire subscription
$profile.setKey('hobbies[0].name', 'Scrapbooking');

// But this one will fire subscription
$profile.setKey('hobbies[0].friends[0].name', 'Leslie Knope');

Lazy Stores

Nano Stores 一个独有的特性是每个 state 有两个模式:

  • Mount: 当 store 有监听器时,store 会进入 mount 模式,这时候 store 会立即触发监听器
  • Disabled: 当 store 没有监听器时,store 会进入 disabled 模式,这时候 store 不会触发监听器

Nano Stores 的目标是将组件中的逻辑移植到 store 中,stores 可以监听 URL 的变化或者网络连接的简历。Mount 和 Disabled 模式可以让你在 UI 需要的时候使用资源

onMount 用来设置 mount 或者 disabled 状态的回调

import { onMount } from 'nanostores';

onMount($profile, () => {
// Mount mode
return () => {
// Disabled mode
};
});

出于性能考虑,store 会在最后一个监听取消订阅后,延迟一秒后进入 disabled 模式。

import { cleanStores, keepMount } from 'nanostores';
import { $profile } from './profile.js';

afterEach(() => {
cleanStores($profile);
});

it('is anonymous from the beginning', () => {
keepMount($profile);
// Checks
});

Computed Stores

Computed Stores 依赖其他 store

import { computed } from 'nanostores';
import { $users } from './users.js';

export const $admins = computed($users, (users) => {
// This callback will be called on every `users` changes
return users.filter((user) => user.isAdmin);
});

你可以将多个 store 组合

import { $lastVisit } from './lastVisit.js';
import { $posts } from './posts.js';

export const newPosts = computed([$lastVisit, $posts], (lastVisit, posts) => {
return posts.filter((post) => post.publishedAt > lastVisit);
});

Actions

Action 会改变 store,比较适合用来处理业务逻辑

import { action } from 'nanostores';

export const increase = action($counter, 'increase', (store, add) => {
if (validateMax(store.get() + add)) {
store.set(store.get() + add);
}
return store.get();
});

increase(1); //=> 1
increase(5); //=> 6

All running async actions are tracked by allTasks(). It can simplify tests with chains of actions.

import { allTasks } from 'nanostores';

renameAllPosts();
await allTasks();

Tasks

startTask 和 task()可以在 store 初始化阶段用来标记异步操作

import { task } from 'nanostores';

onMount($post, () => {
task(async () => {
$post.set(await loadPost());
});
});

You can wait for all ongoing tasks end in tests or SSR with await allTasks().

import { allTasks } from 'nanostores';

$post.listen(() => {}); // Move store to active mode to start data loading
await allTasks();

const html = ReactDOMServer.renderToString(<App />);

Async actions will be wrapped to task() automatically.

rename($post1, 'New title');
rename($post2, 'New title');
await allTasks();

Store Events

Each store has a few events, which you listen:

  • onMount(store, cb):第一个监听
  • onStart(store, cb):最好使用 onMount for simple lazy stores
  • onStop(store, cb): last listener was unsubscribed. Low-level method. It is better to use onMount for simple lazy stores.
  • onSet(store, cb): before applying any changes to the store.
  • onNotify(store, cb): before notifying store’s listeners about changes.
  • onAction(store, cb): start, end and errors of asynchronous actions.

onSet and onNotify events has abort() function to prevent changes or notification.

import { onSet } from 'nanostores';

onSet($store, ({ newValue, abort }) => {
if (!validate(newValue)) {
abort();
}
});

onAction event has two event handlers as properties inside:

  • onError that catches uncaught errors during the execution of actions.
  • onEnd after events has been resolved or rejected.
import { onAction } from 'nanostores';

onAction($store, ({ id, actionName, onError, onEnd }) => {
console.log(`Action ${actionName} was started`);
onError(({ error }) => {
console.error(`Action ${actionName} was failed`, error);
});
onEnd(() => {
console.log(`Action ${actionName} was stopped`);
});
});

Event listeners can communicate with payload.shared object.

用法整理

React & Preact

Use @nanostores/react or @nanostores/preact package and useStore() hook to get store’s value and re-render component on store’s changes.

import { useStore } from '@nanostores/react'; // or '@nanostores/preact'
import { $profile } from '../stores/profile.js';

export const Header = ({ postId }) => {
const profile = useStore($profile);
return <header>Hi, {profile.name}</header>;
};

Vue

Use @nanostores/vue and useStore() composable function to get store’s value and re-render component on store’s changes.

<script setup>
import { useStore } from '@nanostores/vue';
import { $profile } from '../stores/profile.js';

const props = defineProps(['postId']);

const profile = useStore($profile);
</script>

<template>
<header>Hi, {{ profile.name }}</header>
</template>

最佳实践

  • Stores are not only to keep values. You can use them to track time, to load data from server.
import { atom, onMount } from 'nanostores';

export const $currentTime = atom < number > Date.now();

onMount($currentTime, () => {
$currentTime.set(Date.now());
const updating = setInterval(() => {
$currentTime.set(Date.now());
}, 1000);
return () => {
clearInterval(updating);
};
});
  • Use derived stores to create chains of reactive computations.
import { computed } from 'nanostores';
import { $currentTime } from './currentTime.js';

const appStarted = Date.now();

export const $userInApp = computed($currentTime, (currentTime) => {
return currentTime - appStarted;
});
  • 通过使用 listen 方式将 action 与响应行为分离,
const increase = action($counter, 'increase', store => {
store.set(store.get() + 1)
- printCounter(store.get())
}

+ $counter.listen(counter => {
+ printCounter(counter)
+ })

action 不是唯一的数据来源,通过分离,你可以响应任何来源的 store 变化

  • 非测试场景,减少 get()使用
- const { userId } = $profile.get()
+ const { userId } = useStore($profile)

已知问题

ESM

Nano Stores use ESM-only package. You need to use ES modules in your application to import Nano Stores.

In Next.js ≥11.1 you can alternatively use the esmExternals config option.

For old Next.js you need to use next-transpile-modules to fix lack of ESM support in Next.js.