Skip to content

Commit

Permalink
Merge branch 'main' into patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
dai-shi authored Dec 31, 2023
2 parents caee831 + 517524d commit cf47d64
Show file tree
Hide file tree
Showing 11 changed files with 462 additions and 455 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ you can use this suggested workflow:
- Create failing tests for your fix or new feature;
- Implement your changes and confirm that all test are passing.
You can run the tests continuously during development
with the `yarn test:dev` command.
with the `yarn test` command.
- If you want to test it in a React project:
- Either use `yarn link`, or
- Use the `yalc` package.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/flux-inspired-practice.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,4 @@ import { redux } from 'zustand/middleware'
const useReduxStore = create(redux(reducer, initialState))
```

Another way to update the store could be through functions wrapping the state functions. These could also handle side-effects of actions. For example, with HTTP-calls. To use Zustand in a none-reactive way, see [the readme](https://github.com/pmndrs/zustand#readingwriting-state-and-reacting-to-changes-outside-of-components).
Another way to update the store could be through functions wrapping the state functions. These could also handle side-effects of actions. For example, with HTTP-calls. To use Zustand in a non-reactive way, see [the readme](https://github.com/pmndrs/zustand#readingwriting-state-and-reacting-to-changes-outside-of-components).
22 changes: 7 additions & 15 deletions docs/guides/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,15 +443,9 @@ const bearStore = createStore<BearState>()((set) => ({
}))

function useBearStore(): BearState
function useBearStore<T>(
selector: (state: BearState) => T,
equals?: (a: T, b: T) => boolean,
): T
function useBearStore<T>(
selector?: (state: BearState) => T,
equals?: (a: T, b: T) => boolean,
) {
return useStore(bearStore, selector!, equals)
function useBearStore<T>(selector: (state: BearState) => T): T
function useBearStore<T>(selector?: (state: BearState) => T) {
return useStore(bearStore, selector!)
}
```
Expand All @@ -471,15 +465,13 @@ const bearStore = createStore<BearState>()((set) => ({
increase: (by) => set((state) => ({ bears: state.bears + by })),
}))

const createBoundedUseStore = ((store) => (selector, equals) =>
useStore(store, selector as never, equals)) as <S extends StoreApi<unknown>>(
const createBoundedUseStore = ((store) => (selector) => useStore(store)) as <
S extends StoreApi<unknown>,
>(
store: S,
) => {
(): ExtractState<S>
<T>(
selector: (state: ExtractState<S>) => T,
equals?: (a: T, b: T) => boolean,
): T
<T>(selector: (state: ExtractState<S>) => T): T
}

type ExtractState<S> = S extends { getState: () => infer X } ? X : never
Expand Down
4 changes: 2 additions & 2 deletions docs/integrations/immer-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,5 @@ Zustand will skip calling the subscriptions.

## CodeSandbox Demo

- [Basic](https://codesandbox.io/s/zustand-updating-draft-states-basic-demo-zkp22g),
- [Advanced](https://codesandbox.io/s/zustand-updating-draft-states-advanced-demo-3znqzk).
- [Basic](https://codesandbox.io/p/sandbox/zustand-updating-draft-states-basic-demo-forked-96mkdw),
- [Advanced](https://codesandbox.io/p/sandbox/zustand-updating-draft-states-advanced-demo-forked-phkzzg).
11 changes: 3 additions & 8 deletions docs/integrations/persisting-store-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ export const useBoundStore = create(
)
```

If you're using a type that JSON.stringify() doesn't support, you'll need to write your own serialization/deserialization code. However, if this is tedious, you can use third-party libraries to serialize and deserialize different types of data.
If you're using a type that `JSON.stringify()` doesn't support, you'll need to write your own serialization/deserialization code. However, if this is tedious, you can use third-party libraries to serialize and deserialize different types of data.

For example, [Superjson](https://github.com/blitz-js/superjson) can serialize data along with its type, allowing the data to be parsed back to its original type upon deserialization

Expand Down Expand Up @@ -735,15 +735,10 @@ export const useBearStore = create<MyState>()(

### How do I use it with Map and Set

With the previous persist API, you would use `serialize`/`deserialize`
to deal with `Map` and `Set` and convert them into
an Array so they could be parsed into proper JSON.
In order to persist object types such as `Map` and `Set`, they will need to be converted to JSON-serializable types such as an `Array` which can be done by defining a custom `storage` engine.

The new persist API has deprecated `serialize`/`deserialize`.

Now, you will need to use the `storage` prop.
Let's say your state uses `Map` to handle a list of `transactions`,
then you can convert the Map into an Array in the storage prop:
then you can convert the `Map` into an `Array` in the `storage` prop which is shown below:

```ts

Expand Down
1 change: 1 addition & 0 deletions docs/integrations/third-party-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ This can be done using third-party libraries created by the community.
- [zustand-yjs](https://github.com/tandem-pt/zustand-yjs) — Zustand stores for Yjs structures.
- [zusteller](https://github.com/timkindberg/zusteller) — Your global state savior. "Just hooks" + Zustand.
- [zustood](https://github.com/udecode/zustood) — 🐻‍❄️ A modular store factory using Zustand.
- [zusty](https://github.com/oslabs-beta/Zusty) - Zustand tool to assist debugging with time travel, action logs, state snapshots, store view, render time metrics and state component tree.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,15 +251,15 @@
"vitest": "^0.34.6"
},
"peerDependencies": {
"immer": ">=9.0",
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"immer": {
"@types/react": {
"optional": true
},
"@types/react": {
"immer": {
"optional": true
},
"react": {
Expand Down
5 changes: 3 additions & 2 deletions tests/shallow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { act, fireEvent, render } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/vanilla/shallow'

describe('types', () => {
it('works with useBoundStore and array selector (#1107)', () => {
const useBoundStore = create(() => ({
const useBoundStore = createWithEqualityFn(() => ({
villages: [] as { name: string }[],
}))
const Component = () => {
Expand All @@ -18,7 +19,7 @@ describe('types', () => {
})

it('works with useBoundStore and string selector (#1107)', () => {
const useBoundStore = create(() => ({
const useBoundStore = createWithEqualityFn(() => ({
refetchTimestamp: '',
}))
const Component = () => {
Expand Down
122 changes: 4 additions & 118 deletions tests/subscribe.test.tsx
Original file line number Diff line number Diff line change
@@ -1,123 +1,9 @@
import { describe, expect, it, vi } from 'vitest'
import { describe, expect, it } from 'vitest'
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'

describe('subscribe()', () => {
it('should not be called if new state identity is the same', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, subscribe } = create(() => initialState)

subscribe(spy)
setState(initialState)
expect(spy).not.toHaveBeenCalled()
})

it('should be called if new state identity is different', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, getState, subscribe } = create(() => initialState)

subscribe(spy)
setState({ ...getState() })
expect(spy).toHaveBeenCalledWith(initialState, initialState)
})

it('should not be called when state slice is the same', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, subscribe } = create(
subscribeWithSelector(() => initialState),
)

subscribe((s) => s.value, spy)
setState({ other: 'b' })
expect(spy).not.toHaveBeenCalled()
})

it('should be called when state slice changes', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, subscribe } = create(
subscribeWithSelector(() => initialState),
)

subscribe((s) => s.value, spy)
setState({ value: initialState.value + 1 })
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(initialState.value + 1, initialState.value)
})

it('should not be called when equality checker returns true', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, subscribe } = create(
subscribeWithSelector(() => initialState),
)

subscribe((s) => s, spy, { equalityFn: () => true })
setState({ value: initialState.value + 2 })
expect(spy).not.toHaveBeenCalled()
})

it('should be called when equality checker returns false', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, subscribe } = create(
subscribeWithSelector(() => initialState),
)

subscribe((s) => s.value, spy, { equalityFn: () => false })
setState({ value: initialState.value + 2 })
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(initialState.value + 2, initialState.value)
})

it('should unsubscribe correctly', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, subscribe } = create(
subscribeWithSelector(() => initialState),
)

const unsub = subscribe((s) => s.value, spy)

setState({ value: initialState.value + 1 })
unsub()
setState({ value: initialState.value + 2 })

expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(initialState.value + 1, initialState.value)
})

it('should keep consistent behavior with equality check', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { getState, setState, subscribe } = create(
subscribeWithSelector(() => initialState),
)

const isRoughEqual = (x: number, y: number) => Math.abs(x - y) < 1
setState({ value: 0 })
spy.mockReset()
const spy2 = vi.fn()
let prevValue = getState().value
const unsub = subscribe((s) => {
if (isRoughEqual(prevValue, s.value)) {
// skip assuming values are equal
return
}
spy(s.value, prevValue)
prevValue = s.value
})
const unsub2 = subscribe((s) => s.value, spy2, { equalityFn: isRoughEqual })
setState({ value: 0.5 })
setState({ value: 1 })
unsub()
unsub2()
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(1, 0)
expect(spy2).toHaveBeenCalledTimes(1)
expect(spy2).toHaveBeenCalledWith(1, 0)
it('should correctly have access to subscribe', () => {
const { subscribe } = create(() => ({ value: 1 }))
expect(typeof subscribe).toBe('function')
})
})
123 changes: 123 additions & 0 deletions tests/vanilla/subscribe.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { describe, expect, it, vi } from 'vitest'
import { subscribeWithSelector } from 'zustand/middleware'
import { createStore } from 'zustand/vanilla'

describe('subscribe()', () => {
it('should not be called if new state identity is the same', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, subscribe } = createStore(() => initialState)

subscribe(spy)
setState(initialState)
expect(spy).not.toHaveBeenCalled()
})

it('should be called if new state identity is different', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, getState, subscribe } = createStore(() => initialState)

subscribe(spy)
setState({ ...getState() })
expect(spy).toHaveBeenCalledWith(initialState, initialState)
})

it('should not be called when state slice is the same', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, subscribe } = createStore(
subscribeWithSelector(() => initialState),
)

subscribe((s) => s.value, spy)
setState({ other: 'b' })
expect(spy).not.toHaveBeenCalled()
})

it('should be called when state slice changes', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, subscribe } = createStore(
subscribeWithSelector(() => initialState),
)

subscribe((s) => s.value, spy)
setState({ value: initialState.value + 1 })
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(initialState.value + 1, initialState.value)
})

it('should not be called when equality checker returns true', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, subscribe } = createStore(
subscribeWithSelector(() => initialState),
)

subscribe((s) => s, spy, { equalityFn: () => true })
setState({ value: initialState.value + 2 })
expect(spy).not.toHaveBeenCalled()
})

it('should be called when equality checker returns false', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, subscribe } = createStore(
subscribeWithSelector(() => initialState),
)

subscribe((s) => s.value, spy, { equalityFn: () => false })
setState({ value: initialState.value + 2 })
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(initialState.value + 2, initialState.value)
})

it('should unsubscribe correctly', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { setState, subscribe } = createStore(
subscribeWithSelector(() => initialState),
)

const unsub = subscribe((s) => s.value, spy)

setState({ value: initialState.value + 1 })
unsub()
setState({ value: initialState.value + 2 })

expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(initialState.value + 1, initialState.value)
})

it('should keep consistent behavior with equality check', () => {
const spy = vi.fn()
const initialState = { value: 1, other: 'a' }
const { getState, setState, subscribe } = createStore(
subscribeWithSelector(() => initialState),
)

const isRoughEqual = (x: number, y: number) => Math.abs(x - y) < 1
setState({ value: 0 })
spy.mockReset()
const spy2 = vi.fn()
let prevValue = getState().value
const unsub = subscribe((s) => {
if (isRoughEqual(prevValue, s.value)) {
// skip assuming values are equal
return
}
spy(s.value, prevValue)
prevValue = s.value
})
const unsub2 = subscribe((s) => s.value, spy2, { equalityFn: isRoughEqual })
setState({ value: 0.5 })
setState({ value: 1 })
unsub()
unsub2()
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(1, 0)
expect(spy2).toHaveBeenCalledTimes(1)
expect(spy2).toHaveBeenCalledWith(1, 0)
})
})
Loading

0 comments on commit cf47d64

Please sign in to comment.