Skip to content

Commit

Permalink
feat(vanilla): non-object state (pmndrs#1144)
Browse files Browse the repository at this point in the history
* feat: non-object state

* add test

* prefer unknown

* fix types for persist and subscribeWithSelector

* no Cast in persist and subscribeWithSelector

* simplify immer type

* simplify devtools type

* simplify redux type

* simplify type with looser action

* fix StoreApi type parameter

* fix types
  • Loading branch information
dai-shi authored Aug 18, 2022
1 parent ed12c7e commit c60a535
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 63 deletions.
4 changes: 2 additions & 2 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from 'react'
import { StoreApi, useStore } from 'zustand'

type UseContextStore<S extends StoreApi> = {
type UseContextStore<S extends StoreApi<unknown>> = {
(): ExtractState<S>
<U>(
selector: (state: ExtractState<S>) => U,
Expand All @@ -20,7 +20,7 @@ type ExtractState<S> = S extends { getState: () => infer T } ? T : never

type WithoutCallSignature<T> = { [K in keyof T]: T[K] }

function createContext<S extends StoreApi>() {
function createContext<S extends StoreApi<unknown>>() {
const ZustandContext = reactCreateContext<S | undefined>(undefined)

const Provider = ({
Expand Down
20 changes: 11 additions & 9 deletions src/middleware/devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ type Message = {
state?: any
}

type Write<T extends object, U extends object> = Omit<T, keyof U> & U
type Cast<T, U> = T extends U ? T : U
type Write<T, U> = Omit<T, keyof U> & U
type TakeTwo<T> = T extends []
? [undefined, undefined]
: T extends [unknown]
Expand All @@ -37,7 +36,7 @@ type TakeTwo<T> = T extends []
? [A0?, A1?]
: never

type WithDevtools<S> = Write<Cast<S, object>, StoreDevtools<S>>
type WithDevtools<S> = Write<S, StoreDevtools<S>>

type StoreDevtools<S> = S extends {
setState: (...a: infer Sa) => infer Sr
Expand All @@ -49,6 +48,9 @@ type StoreDevtools<S> = S extends {
}
: never

const isObjectWithTypeProperty = (x: unknown): x is { type: unknown } =>
x !== null && typeof x === 'object' && 'type' in x

export interface DevtoolsOptions {
enabled?: boolean
anonymousActionType?: string
Expand All @@ -69,7 +71,7 @@ export interface DevtoolsOptions {
}

type Devtools = <
T extends object,
T,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = []
>(
Expand All @@ -84,7 +86,7 @@ declare module '../vanilla' {
}
}

type DevtoolsImpl = <T extends object>(
type DevtoolsImpl = <T>(
storeInitializer: PopArgument<StateCreator<T, [], []>>,
devtoolsOptions?: DevtoolsOptions
) => PopArgument<StateCreator<T, [], []>>
Expand All @@ -95,7 +97,7 @@ type PopArgument<T extends (...a: never[]) => unknown> = T extends (
? (...a: A) => R
: never

export type NamedSet<T extends object> = WithDevtools<StoreApi<T>>['setState']
export type NamedSet<T> = WithDevtools<StoreApi<T>>['setState']

const devtoolsImpl: DevtoolsImpl =
(fn, devtoolsOptions = {}) =>
Expand Down Expand Up @@ -132,9 +134,9 @@ const devtoolsImpl: DevtoolsImpl =
extension.send(
nameOrAction === undefined
? { type: anonymousActionType || 'anonymous' }
: typeof nameOrAction === 'string'
? { type: nameOrAction }
: nameOrAction,
: isObjectWithTypeProperty(nameOrAction)
? nameOrAction
: { type: nameOrAction },
get()
)
return r
Expand Down
9 changes: 4 additions & 5 deletions src/middleware/immer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Draft, produce } from 'immer'
import { StateCreator, StoreMutatorIdentifier } from '../vanilla'

type Immer = <
T extends object,
T,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = []
>(
Expand All @@ -17,8 +17,7 @@ declare module '../vanilla' {
}
}

type Write<T extends object, U extends object> = Omit<T, keyof U> & U
type Cast<T, U> = T extends U ? T : U
type Write<T, U> = Omit<T, keyof U> & U
type SkipTwo<T> = T extends []
? []
: T extends [unknown]
Expand All @@ -33,7 +32,7 @@ type SkipTwo<T> = T extends []
? A
: never

type WithImmer<S> = Write<Cast<S, object>, StoreImmer<S>>
type WithImmer<S> = Write<S, StoreImmer<S>>

type StoreImmer<S> = S extends {
getState: () => infer T
Expand All @@ -56,7 +55,7 @@ type PopArgument<T extends (...a: never[]) => unknown> = T extends (
? (...a: A) => R
: never

type ImmerImpl = <T extends object>(
type ImmerImpl = <T>(
storeInitializer: PopArgument<StateCreator<T, [], []>>
) => PopArgument<StateCreator<T, [], []>>

Expand Down
11 changes: 5 additions & 6 deletions src/middleware/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export interface PersistOptions<S, PersistedState = S> {

type PersistListener<S> = (state: S) => void

type StorePersist<S extends object, Ps> = {
type StorePersist<S, Ps> = {
persist: {
setOptions: (options: Partial<PersistOptions<S, Ps>>) => void
clearStorage: () => void
Expand Down Expand Up @@ -297,7 +297,7 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
}

type Persist = <
T extends object,
T,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = [],
U = T
Expand All @@ -312,14 +312,13 @@ declare module '../vanilla' {
}
}

type Write<T extends object, U extends object> = Omit<T, keyof U> & U
type Cast<T, U> = T extends U ? T : U
type Write<T, U> = Omit<T, keyof U> & U

type WithPersist<S, A> = S extends { getState: () => infer T }
? Write<S, StorePersist<Cast<T, object>, A>>
? Write<S, StorePersist<T, A>>
: never

type PersistImpl = <T extends object>(
type PersistImpl = <T>(
storeInitializer: PopArgument<StateCreator<T, [], []>>,
options: PersistOptions<T, T>
) => PopArgument<StateCreator<T, [], []>>
Expand Down
30 changes: 14 additions & 16 deletions src/middleware/redux.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
import { StateCreator, StoreMutatorIdentifier } from '../vanilla'
import { NamedSet } from './devtools'

type Write<T extends object, U extends object> = Omit<T, keyof U> & U
type Cast<T, U> = T extends U ? T : U
type Write<T, U> = Omit<T, keyof U> & U

type Action = {
type: unknown
}

type ReduxState<A extends Action> = {
type ReduxState<A> = {
dispatch: StoreRedux<A>['dispatch']
}

type StoreRedux<A extends Action> = {
type StoreRedux<A> = {
dispatch: (a: A) => A
dispatchFromDevtools: true
}

type WithRedux<S, A> = Write<Cast<S, object>, StoreRedux<Cast<A, Action>>>
type WithRedux<S, A> = Write<S, StoreRedux<A>>

type Redux = <
T extends object,
A extends Action,
Cms extends [StoreMutatorIdentifier, unknown][] = []
>(
type Redux = <T, A, Cms extends [StoreMutatorIdentifier, unknown][] = []>(
reducer: (state: T, action: A) => T,
initialState: T
) => StateCreator<Write<T, ReduxState<A>>, Cms, [['zustand/redux', A]]>
Expand All @@ -40,16 +31,23 @@ type PopArgument<T extends (...a: never[]) => unknown> = T extends (
? (...a: A) => R
: never

type ReduxImpl = <T extends object, A extends Action>(
type ReduxImpl = <T, A>(
reducer: (state: T, action: A) => T,
initialState: T
) => PopArgument<StateCreator<T & ReduxState<A>, [], []>>

const isObjectWithTypeProperty = (x: unknown): x is { type: unknown } =>
x !== null && typeof x === 'object' && 'type' in x

const reduxImpl: ReduxImpl = (reducer, initial) => (set, _get, api) => {
type S = typeof initial
type A = Parameters<typeof reducer>[1]
;(api as any).dispatch = (action: A) => {
;(set as NamedSet<S>)((state: S) => reducer(state, action), false, action)
;(set as NamedSet<S>)(
(state: S) => reducer(state, action),
false,
isObjectWithTypeProperty(action) ? action : { type: action }
)
return action
}
;(api as any).dispatchFromDevtools = true
Expand Down
9 changes: 4 additions & 5 deletions src/middleware/subscribeWithSelector.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { StateCreator, StoreMutatorIdentifier } from '../vanilla'

type SubscribeWithSelector = <
T extends object,
T,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = []
>(
Expand All @@ -12,11 +12,10 @@ type SubscribeWithSelector = <
>
) => StateCreator<T, Mps, [['zustand/subscribeWithSelector', never], ...Mcs]>

type Write<T extends object, U extends object> = Omit<T, keyof U> & U
type Cast<T, U> = T extends U ? T : U
type Write<T, U> = Omit<T, keyof U> & U

type WithSelectorSubscribe<S> = S extends { getState: () => infer T }
? Write<S, StoreSubscribeWithSelector<Cast<T, object>>>
? Write<S, StoreSubscribeWithSelector<T>>
: never

declare module '../vanilla' {
Expand All @@ -26,7 +25,7 @@ declare module '../vanilla' {
}
}

type StoreSubscribeWithSelector<T extends object> = {
type StoreSubscribeWithSelector<T> = {
subscribe: {
(listener: (selectedState: T, previousSelectedState: T) => void): () => void
<U>(
Expand Down
25 changes: 13 additions & 12 deletions src/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,21 @@ const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports

type ExtractState<S> = S extends { getState: () => infer T } ? T : never

type WithReact<S extends StoreApi> = S & {
type WithReact<S extends StoreApi<unknown>> = S & {
getServerState?: () => ExtractState<S>
}

export function useStore<S extends WithReact<StoreApi>>(api: S): ExtractState<S>
export function useStore<S extends WithReact<StoreApi<unknown>>>(
api: S
): ExtractState<S>

export function useStore<S extends WithReact<StoreApi>, U>(
export function useStore<S extends WithReact<StoreApi<unknown>>, U>(
api: S,
selector: (state: ExtractState<S>) => U,
equalityFn?: (a: U, b: U) => boolean
): U

export function useStore<TState extends object, StateSlice>(
export function useStore<TState, StateSlice>(
api: WithReact<StoreApi<TState>>,
selector: (state: TState) => StateSlice = api.getState as any,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean
Expand All @@ -43,7 +45,7 @@ export function useStore<TState extends object, StateSlice>(
return slice
}

export type UseBoundStore<S extends WithReact<StoreApi>> = {
export type UseBoundStore<S extends WithReact<StoreApi<unknown>>> = {
(): ExtractState<S>
<U>(
selector: (state: ExtractState<S>) => U,
Expand All @@ -52,16 +54,16 @@ export type UseBoundStore<S extends WithReact<StoreApi>> = {
} & S

type Create = {
<T extends object, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
<T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>
): UseBoundStore<Mutate<StoreApi<T>, Mos>>
<T extends object>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>
) => UseBoundStore<Mutate<StoreApi<T>, Mos>>
<S extends StoreApi>(store: S): UseBoundStore<S>
<S extends StoreApi<unknown>>(store: S): UseBoundStore<S>
}

const createImpl = <T extends object>(createState: StateCreator<T, [], []>) => {
const createImpl = <T>(createState: StateCreator<T, [], []>) => {
const api =
typeof createState === 'function' ? createStore(createState) : createState

Expand All @@ -73,8 +75,7 @@ const createImpl = <T extends object>(createState: StateCreator<T, [], []>) => {
return useBoundStore
}

const create = (<T extends object>(
createState: StateCreator<T, [], []> | undefined
) => (createState ? createImpl(createState) : createImpl)) as Create
const create = (<T>(createState: StateCreator<T, [], []> | undefined) =>
createState ? createImpl(createState) : createImpl) as Create

export default create
17 changes: 9 additions & 8 deletions src/vanilla.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type SetStateInternal<T> = {
): void
}['_']

export interface StoreApi<T extends object = object> {
export interface StoreApi<T> {
setState: SetStateInternal<T>
getState: () => T
subscribe: (listener: (state: T, prevState: T) => void) => () => void
Expand All @@ -21,7 +21,7 @@ export type Mutate<S, Ms> = Ms extends []
: never

export type StateCreator<
T extends object,
T,
Mis extends [StoreMutatorIdentifier, unknown][] = [],
Mos extends [StoreMutatorIdentifier, unknown][] = [],
U = T
Expand All @@ -37,17 +37,17 @@ export interface StoreMutators<S, A> {}
export type StoreMutatorIdentifier = keyof StoreMutators<unknown, unknown>

type CreateStore = {
<T extends object, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
<T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>
): Mutate<StoreApi<T>, Mos>

<T extends object>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>
) => Mutate<StoreApi<T>, Mos>
}

type CreateStoreImpl = <
T extends object,
T,
Mos extends [StoreMutatorIdentifier, unknown][] = []
>(
initializer: StateCreator<T, [], Mos>
Expand All @@ -74,9 +74,10 @@ const createStoreImpl: CreateStoreImpl = (createState) => {
: partial
if (nextState !== state) {
const previousState = state
state = replace
? (nextState as TState)
: Object.assign({}, state, nextState)
state =
replace ?? typeof nextState !== 'object'
? (nextState as TState)
: Object.assign({}, state, nextState)
listeners.forEach((listener) => listener(state, previousState))
}
}
Expand Down
22 changes: 22 additions & 0 deletions tests/basic.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -576,3 +576,25 @@ it('ensures a subscriber is not mistakenly overwritten', async () => {
expect((await findAllByText('count1: 1')).length).toBe(2)
expect((await findAllByText('count2: 1')).length).toBe(1)
})

it('works with non-object state', async () => {
const useCount = create(() => 1)
const inc = () => useCount.setState((c) => c + 1)

const Counter = () => {
const count = useCount()
return (
<>
<div>count: {count}</div>
<button onClick={inc}>button</button>
</>
)
}

const { getByText, findByText } = render(<Counter />)

await findByText('count: 1')

fireEvent.click(getByText('button'))
await findByText('count: 2')
})

0 comments on commit c60a535

Please sign in to comment.