Skip to content

Commit

Permalink
feat: support exclude in store.restore options.
Browse files Browse the repository at this point in the history
  • Loading branch information
vikiboss committed Jan 2, 2025
1 parent f6d3560 commit cb5985f
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 25 deletions.
2 changes: 0 additions & 2 deletions docs/en/guide/usage/react.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
17 changes: 17 additions & 0 deletions docs/en/reference/basic/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,20 @@ Restore the Store to its initial state.
```tsx
store.restore();
```
Optional params for configuring restore behavior.
```tsx
export interface RestoreOptions<State extends object> {
/**
* 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'] });
```
3 changes: 0 additions & 3 deletions docs/zh-cn/guide/usage/react.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -198,4 +196,3 @@ export default function App() {
)
}
```

17 changes: 17 additions & 0 deletions docs/zh-cn/reference/basic/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,20 @@ export interface WithUseSubscribeContributes<State extends object> {
```tsx
store.restore();
```
可选传入一个 Options 对象,用于配置恢复行为
```tsx
export interface RestoreOptions<State extends object> {
/**
* 在恢复初始化状态时,保留某些**顶层**属性键名的状态。
*
* @since 0.2.5
*/
exclude?: (keyof State)[]
}
```
```tsx
store.restore({ exclude: ['count'] });
```
23 changes: 23 additions & 0 deletions packages/reactive/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <button onClick={increaseCount}>Count is {count}</button>
* }
*/
createWithHooks as create,
/**
* @since 0.2.0
Expand Down
29 changes: 18 additions & 11 deletions packages/reactive/src/react/create-with-hooks.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -12,20 +15,24 @@ export interface StoreUseSnapshot<State> {
(options: SnapshotOptions<State>): State
<StateSlice>(selector: SnapshotSelector<State, StateSlice>): StateSlice
<StateSlice>(selector: undefined, options: SnapshotOptions<StateSlice>): State
<StateSlice>(selector: SnapshotSelector<State, StateSlice>, options: SnapshotOptions<StateSlice>): StateSlice
<StateSlice>(
selector: SnapshotSelector<State, StateSlice>,
options: SnapshotOptions<StateSlice>
): StateSlice
}

export type Store<State extends object> = ExpandType<
VanillaStore<State> &
WithSubscribeContributes<State> &
WithSnapshotContributes<State> &
WithUseSnapshotContributes<State> &
WithUseSubscribeContributes<State>
>
export interface Store<State extends object>
extends ExpandType<
VanillaStore<State> &
WithSubscribeContributes<State> &
WithSnapshotContributes<State> &
WithUseSnapshotContributes<State> &
WithUseSubscribeContributes<State>
> {}

export function createWithHooks<State extends object>(
initialState: State,
options: StoreCreateOptions = {},
options: StoreCreateOptions = {}
): Store<State> {
return withUseSnapshot(withUseSubscribe(createVanilla<State>(initialState, options)))
}
70 changes: 68 additions & 2 deletions packages/reactive/src/vanilla/create.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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)
Expand Down Expand Up @@ -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',
Expand Down
22 changes: 17 additions & 5 deletions packages/reactive/src/vanilla/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,27 @@ export type StoreSubscriber<State extends object> = (
/**
* @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<State extends object> = {
export interface VanillaStore<State extends object> {
/**
* The mutable state object, whose changes will trigger subscribers.
*/
mutate: State
/**
* Restore to initial state.
*/
restore: () => void
restore: (options?: RestoreOptions<State>) => void
}

export interface RestoreOptions<State extends object> {
/**
* Exclude some **top** keys from restoring.
*
* @since 0.2.5
*/
exclude?: (keyof State)[]
}

/**
Expand All @@ -46,14 +55,17 @@ export type VanillaStore<State extends object> = {
*/
export function createVanilla<State extends object>(
initState: State,
options: StoreCreateOptions = {},
options: StoreCreateOptions = {}
): VanillaStore<State> & WithSubscribeContributes<State> & WithSnapshotContributes<State> {
const proxyState = proxy(initState)

function restore() {
function restore(options: RestoreOptions<State> = {}) {
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]
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/reactive/src/vanilla/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { snapshot } from './snapshot.js'

import type { StoreListener } from './proxy.js'

export type ChangeItem<State> = {
export interface ChangeItem<State> {
props: PropertyKey[]
propsPath: string
previous: unknown
Expand All @@ -21,7 +21,7 @@ export type SubscribeListener<State> = (changes: ChangeItem<State>, version?: nu
export function subscribe<State extends object>(
proxyState: State,
callback: SubscribeListener<State>,
notifyInSync?: boolean,
notifyInSync?: boolean
) {
let promise: Promise<void> | undefined
let previousState = snapshot(proxyState)
Expand Down

0 comments on commit cb5985f

Please sign in to comment.