diff --git a/docs/en/guide/usage/react.md b/docs/en/guide/usage/react.md index deaddef..6daf37d 100644 --- a/docs/en/guide/usage/react.md +++ b/docs/en/guide/usage/react.md @@ -182,8 +182,6 @@ export default function App() { If needed, you can easily **restore** to the initial state through `store.restore()`, for example, resetting the state when the component unmounts. -`store.restore()` uses the newer [structuredClone API](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone), consider adding a [polyfill](https://github.com/ungap/structured-clone) if necessary. - ```tsx import { useUnmount } from '@shined/react-use' import { store } from './store' diff --git a/docs/en/reference/basic/create.md b/docs/en/reference/basic/create.md index aedcb00..b2270e5 100644 --- a/docs/en/reference/basic/create.md +++ b/docs/en/reference/basic/create.md @@ -209,3 +209,20 @@ Restore the Store to its initial state. ```tsx store.restore(); ``` + +Optional params for configuring restore behavior. + +```tsx +export interface RestoreOptions { + /** + * When restoring the initial state, retain the state of some **top-level** property key names. + * + * @since 0.2.5 + */ + exclude?: (keyof State)[] +} +``` + +```tsx +store.restore({ exclude: ['count'] }); +``` diff --git a/docs/zh-cn/guide/usage/react.md b/docs/zh-cn/guide/usage/react.md index 7b96a4d..ffea5a4 100644 --- a/docs/zh-cn/guide/usage/react.md +++ b/docs/zh-cn/guide/usage/react.md @@ -182,8 +182,6 @@ export default function App() { 如果需要,你也可以通过 `store.restore()` 轻松地**恢复**到初始状态,比如在组件卸载时,重置状态。 -`store.restore()` 中使用了较新的 [structuredClone API](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone),如果有需要,请考虑添加一个 [polyfill](https://github.com/ungap/structured-clone)。 - ```tsx import { useUnmount } from '@shined/react-use' import { store } from './store' @@ -198,4 +196,3 @@ export default function App() { ) } ``` - diff --git a/docs/zh-cn/reference/basic/create.md b/docs/zh-cn/reference/basic/create.md index 2441b3a..cd86e4b 100644 --- a/docs/zh-cn/reference/basic/create.md +++ b/docs/zh-cn/reference/basic/create.md @@ -210,3 +210,20 @@ export interface WithUseSubscribeContributes { ```tsx store.restore(); ``` + +可选传入一个 Options 对象,用于配置恢复行为 + +```tsx +export interface RestoreOptions { + /** + * 在恢复初始化状态时,保留某些**顶层**属性键名的状态。 + * + * @since 0.2.5 + */ + exclude?: (keyof State)[] +} +``` + +```tsx +store.restore({ exclude: ['count'] }); +``` diff --git a/packages/reactive/src/index.ts b/packages/reactive/src/index.ts index 3c232cb..1a715eb 100644 --- a/packages/reactive/src/index.ts +++ b/packages/reactive/src/index.ts @@ -18,6 +18,29 @@ export { } from './vanilla/index.js' export { + /** + * Create a store with React hooks. If you are in a Vanilla JavaScript environment, consider using [createVanilla](https://sheinsight.github.io/reactive/zh-cn/reference/basic/create-vanilla.html) instead. + * + * 创建一个带有 React hooks 的 store。如果你在 Vanilla JavaScript 环境中使用 store,请考虑使用 [createVanilla](https://sheinsight.github.io/reactive/zh-cn/reference/basic/create-vanilla.html) 替代。 + * + * @link https://sheinsight.github.io/reactive/reference/basic/create.html + * + * @example + * + * ```tsx + * import { create } from '@shined/reactive' + * + * const store = create({ count: 0 }) + * + * function increaseCount() { + * store.mutate.count++ + * } + * + * function Counter() { + * const count = store.useSnapshot(s => s.count) + * return + * } + */ createWithHooks as create, /** * @since 0.2.0 diff --git a/packages/reactive/src/react/create-with-hooks.ts b/packages/reactive/src/react/create-with-hooks.ts index 6e60d87..a1966bf 100644 --- a/packages/reactive/src/react/create-with-hooks.ts +++ b/packages/reactive/src/react/create-with-hooks.ts @@ -1,7 +1,10 @@ +import { createVanilla } from '../vanilla/index.js' import { withUseSnapshot, withUseSubscribe } from '../enhancers/react/index.js' -import { createVanilla, withSnapshot, withSubscribe } from '../vanilla/index.js' -import type { WithUseSnapshotContributes, WithUseSubscribeContributes } from '../enhancers/react/index.js' +import type { + WithUseSnapshotContributes, + WithUseSubscribeContributes, +} from '../enhancers/react/index.js' import type { ExpandType } from '../utils/index.js' import type { StoreCreateOptions, VanillaStore } from '../vanilla/create.js' import type { WithSnapshotContributes, WithSubscribeContributes } from '../vanilla/index.js' @@ -12,20 +15,24 @@ export interface StoreUseSnapshot { (options: SnapshotOptions): State (selector: SnapshotSelector): StateSlice (selector: undefined, options: SnapshotOptions): State - (selector: SnapshotSelector, options: SnapshotOptions): StateSlice + ( + selector: SnapshotSelector, + options: SnapshotOptions + ): StateSlice } -export type Store = ExpandType< - VanillaStore & - WithSubscribeContributes & - WithSnapshotContributes & - WithUseSnapshotContributes & - WithUseSubscribeContributes -> +export interface Store + extends ExpandType< + VanillaStore & + WithSubscribeContributes & + WithSnapshotContributes & + WithUseSnapshotContributes & + WithUseSubscribeContributes + > {} export function createWithHooks( initialState: State, - options: StoreCreateOptions = {}, + options: StoreCreateOptions = {} ): Store { return withUseSnapshot(withUseSubscribe(createVanilla(initialState, options))) } diff --git a/packages/reactive/src/vanilla/create.spec.ts b/packages/reactive/src/vanilla/create.spec.ts index 43bd869..f065177 100644 --- a/packages/reactive/src/vanilla/create.spec.ts +++ b/packages/reactive/src/vanilla/create.spec.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vitest } from 'vitest' import { useSnapshot } from '../react/use-snapshot.js' import { create } from './create.js' -import { getSnapshot } from './snapshot.js' +import { snapshot } from './snapshot.js' describe('index', () => { it('create, proxy, useSnapshot and subscribe should be defined', () => { @@ -26,7 +26,7 @@ describe('index', () => { }) const { result } = renderHook(() => useSnapshot(store.mutate)) - expect(result.current).toEqual(getSnapshot(store.mutate)) + expect(result.current).toEqual(snapshot(store.mutate)) const callback = vitest.fn() store.subscribe(callback) @@ -76,6 +76,72 @@ describe('index', () => { }) }) + it('should not restore excluded keys', async () => { + const store = create({ + name: 'Pikachu', + info: { + age: 10, + }, + address: { + city: 'NanJing', + }, + }) + + store.mutate.name = 'Charmander' + store.mutate.info.age = 20 + store.restore({ + exclude: ['name', 'info'], + }) + + await Promise.resolve() + + expect(store.mutate).toMatchObject({ + name: 'Charmander', + info: { + age: 20, + }, + address: { + city: 'NanJing', + }, + }) + + store.mutate.name = 'Charmander' + store.mutate.info.age = 20 + store.restore({ + exclude: ['info'], + }) + + await Promise.resolve() + + expect(store.mutate).toMatchObject({ + name: 'Pikachu', + info: { + age: 20, + }, + address: { + city: 'NanJing', + }, + }) + + store.mutate.name = 'Charmander' + store.mutate.info.age = 20 + store.restore({ + exclude: [], + }) + + await Promise.resolve() + + expect(store.mutate).toMatchObject({ + name: 'Pikachu', + info: { + age: 10, + }, + address: { + city: 'NanJing', + }, + }) + }) + it('should subscribe and unsubscribe', async () => { const store = create({ name: 'Pikachu', diff --git a/packages/reactive/src/vanilla/create.ts b/packages/reactive/src/vanilla/create.ts index 9e21252..49d6b0f 100644 --- a/packages/reactive/src/vanilla/create.ts +++ b/packages/reactive/src/vanilla/create.ts @@ -22,10 +22,10 @@ export type StoreSubscriber = ( /** * @deprecated It is confusing, and makes TS type wrong in callback's `changes` argument. It will be removed in the next major version. */ - selector?: (state: State) => object, + selector?: (state: State) => object ) => () => void -export type VanillaStore = { +export interface VanillaStore { /** * The mutable state object, whose changes will trigger subscribers. */ @@ -33,7 +33,16 @@ export type VanillaStore = { /** * Restore to initial state. */ - restore: () => void + restore: (options?: RestoreOptions) => void +} + +export interface RestoreOptions { + /** + * Exclude some **top** keys from restoring. + * + * @since 0.2.5 + */ + exclude?: (keyof State)[] } /** @@ -46,14 +55,17 @@ export type VanillaStore = { */ export function createVanilla( initState: State, - options: StoreCreateOptions = {}, + options: StoreCreateOptions = {} ): VanillaStore & WithSubscribeContributes & WithSnapshotContributes { const proxyState = proxy(initState) - function restore() { + function restore(options: RestoreOptions = {}) { + const { exclude = [] } = options + const clonedState = deepCloneWithRef(initState) for (const key of Object.keys(clonedState)) { + if (exclude.includes(key as keyof State)) continue proxyState[key as keyof State] = clonedState[key as keyof State] } } diff --git a/packages/reactive/src/vanilla/subscribe.ts b/packages/reactive/src/vanilla/subscribe.ts index adc29a9..970c0fe 100644 --- a/packages/reactive/src/vanilla/subscribe.ts +++ b/packages/reactive/src/vanilla/subscribe.ts @@ -3,7 +3,7 @@ import { snapshot } from './snapshot.js' import type { StoreListener } from './proxy.js' -export type ChangeItem = { +export interface ChangeItem { props: PropertyKey[] propsPath: string previous: unknown @@ -21,7 +21,7 @@ export type SubscribeListener = (changes: ChangeItem, version?: nu export function subscribe( proxyState: State, callback: SubscribeListener, - notifyInSync?: boolean, + notifyInSync?: boolean ) { let promise: Promise | undefined let previousState = snapshot(proxyState)