Comparing version 0.1.3 to 0.2.0
@@ -1,13 +0,21 @@ | ||
declare type StateListener<T> = (state: T) => void; | ||
declare type StateSelector<T, U> = (state: T) => U; | ||
declare type PartialState<T> = Partial<T> | ((state: T) => Partial<T>); | ||
export default function create<State extends Record<string, any>, SetState extends (partialState: PartialState<Record<string, any>>) => void, GetState extends () => Record<string, any>>(createState: (set: SetState, get: GetState) => State): [{ | ||
(): State; | ||
<U>(selector: StateSelector<State, U>, dependencies?: readonly any[] | undefined): U; | ||
}, { | ||
export declare type State = Record<string, any>; | ||
export declare type StateListener<T extends State, U = T> = (state: U) => void; | ||
export declare type StateSelector<T extends State, U> = (state: T) => U; | ||
export declare type PartialState<T extends State> = Partial<T> | ((state: T) => Partial<T>); | ||
export declare type SetState<T extends State> = (partial: PartialState<T>) => void; | ||
export declare type GetState<T extends State> = () => T; | ||
export interface Subscribe<T> { | ||
(listener: StateListener<T>): () => void; | ||
<U>(selector: StateSelector<T, U>, listener: StateListener<T, U>): () => void; | ||
} | ||
export interface UseStore<T> { | ||
(): T; | ||
<U>(selector: StateSelector<T, U>, dependencies?: ReadonlyArray<any>): U; | ||
} | ||
export interface StoreApi<T> { | ||
getState: GetState<T>; | ||
setState: SetState<T>; | ||
subscribe: Subscribe<T>; | ||
destroy: () => void; | ||
getState: () => State; | ||
setState: (partialState: PartialState<State>) => void; | ||
subscribe: (listener: StateListener<State>) => () => void; | ||
}]; | ||
export {}; | ||
} | ||
export default function create<TState extends State>(createState: (set: SetState<State>, get: GetState<State>) => TState): [UseStore<TState>, StoreApi<TState>]; |
@@ -36,7 +36,11 @@ 'use strict'; | ||
var setState = function setState(partialState) { | ||
state = Object.assign({}, state, typeof partialState === 'function' ? partialState(state) : partialState); | ||
listeners.forEach(function (listener) { | ||
return listener(state); | ||
}); | ||
var setState = function setState(partial) { | ||
var partialState = typeof partial === 'function' ? partial(state) : partial; | ||
if (partialState !== state) { | ||
state = Object.assign({}, state, partialState); | ||
listeners.forEach(function (listener) { | ||
return listener(state); | ||
}); | ||
} | ||
}; | ||
@@ -46,8 +50,23 @@ | ||
return state; | ||
}; | ||
}; // Optional selector param goes first so we can infer its return type and use | ||
// it for listener | ||
var subscribe = function subscribe(listener) { | ||
var subscribe = function subscribe(selectorOrListener, listenerOrUndef) { | ||
var listener = selectorOrListener; // Existance of second param means a selector was passed in | ||
if (listenerOrUndef) { | ||
// We know selector is not type StateListener so it must be StateSelector | ||
var selector = selectorOrListener; | ||
var stateSlice = selector(state); | ||
listener = function listener() { | ||
var selectedSlice = selector(state); | ||
if (!shallowEqual(stateSlice, stateSlice = selectedSlice)) listenerOrUndef(stateSlice); | ||
}; | ||
} | ||
listeners.add(listener); | ||
return function () { | ||
listeners["delete"](listener); | ||
return void listeners["delete"](listener); | ||
}; | ||
@@ -58,38 +77,36 @@ }; | ||
listeners.clear(); | ||
state = {}; | ||
}; | ||
function useStore(selector, dependencies) { | ||
// State selector gets entire state if no selector was passed in | ||
var stateSelector = typeof selector === 'function' ? selector : getState; | ||
var selectState = react.useCallback(stateSelector, dependencies); | ||
var selectStateRef = react.useRef(selectState); | ||
var useStore = function useStore(selector, dependencies) { | ||
var selectorRef = react.useRef(selector); | ||
var depsRef = react.useRef(dependencies); | ||
var _useReducer = react.useReducer(reducer, state, selectState), | ||
var _useReducer = react.useReducer(reducer, state, // Optional third argument but required to not be 'undefined' | ||
selector), | ||
stateSlice = _useReducer[0], | ||
dispatch = _useReducer[1]; // Call new selector if it has changed | ||
dispatch = _useReducer[1]; // Need to manually get state slice if selector has changed with no deps or | ||
// deps exist and have changed | ||
if (selectState !== selectStateRef.current) stateSlice = selectState(state); // Store in ref to enable updating without rerunning subscribe/unsubscribe | ||
if (selector && (!dependencies && selector !== selectorRef.current || dependencies && !shallowEqual(dependencies, depsRef.current))) { | ||
stateSlice = selector(state); | ||
} // Update refs synchronously after view has been updated | ||
var stateSliceRef = react.useRef(stateSlice); // Update refs only after view has been updated | ||
react.useLayoutEffect(function () { | ||
selectStateRef.current = selectState; | ||
stateSliceRef.current = stateSlice; | ||
}, [selectState, stateSlice]); // Subscribe/unsubscribe to the store only on mount/unmount | ||
selectorRef.current = selector; | ||
depsRef.current = dependencies; | ||
}, dependencies || [selector]); | ||
react.useLayoutEffect(function () { | ||
return subscribe(function () { | ||
// Use the last selector passed to useStore to get current state slice | ||
var selectedSlice = selectStateRef.current(state); // Shallow compare previous state slice with current and rerender only if changed | ||
if (!shallowEqual(stateSliceRef.current, selectedSlice)) dispatch(selectedSlice); | ||
}); | ||
}, []); | ||
return selector ? subscribe( // Truthy check because it might be possible to set selectorRef to | ||
function () { | ||
return selectorRef.current ? selectorRef.current(state) : state; | ||
}, dispatch) : subscribe(dispatch); // Only resubscribe to the store when changing selector from function to | ||
// undefined or undefined to function | ||
}, [!selector]); | ||
return stateSlice; | ||
} | ||
}; | ||
var state = createState(setState, getState); | ||
var api = { | ||
return [useStore, { | ||
destroy: destroy, | ||
@@ -99,6 +116,5 @@ getState: getState, | ||
subscribe: subscribe | ||
}; | ||
return [useStore, api]; | ||
}]; | ||
} | ||
module.exports = create; |
@@ -1,13 +0,21 @@ | ||
declare type StateListener<T> = (state: T) => void; | ||
declare type StateSelector<T, U> = (state: T) => U; | ||
declare type PartialState<T> = Partial<T> | ((state: T) => Partial<T>); | ||
export default function create<State extends Record<string, any>, SetState extends (partialState: PartialState<Record<string, any>>) => void, GetState extends () => Record<string, any>>(createState: (set: SetState, get: GetState) => State): [{ | ||
(): State; | ||
<U>(selector: StateSelector<State, U>, dependencies?: readonly any[] | undefined): U; | ||
}, { | ||
export declare type State = Record<string, any>; | ||
export declare type StateListener<T extends State, U = T> = (state: U) => void; | ||
export declare type StateSelector<T extends State, U> = (state: T) => U; | ||
export declare type PartialState<T extends State> = Partial<T> | ((state: T) => Partial<T>); | ||
export declare type SetState<T extends State> = (partial: PartialState<T>) => void; | ||
export declare type GetState<T extends State> = () => T; | ||
export interface Subscribe<T> { | ||
(listener: StateListener<T>): () => void; | ||
<U>(selector: StateSelector<T, U>, listener: StateListener<T, U>): () => void; | ||
} | ||
export interface UseStore<T> { | ||
(): T; | ||
<U>(selector: StateSelector<T, U>, dependencies?: ReadonlyArray<any>): U; | ||
} | ||
export interface StoreApi<T> { | ||
getState: GetState<T>; | ||
setState: SetState<T>; | ||
subscribe: Subscribe<T>; | ||
destroy: () => void; | ||
getState: () => State; | ||
setState: (partialState: PartialState<State>) => void; | ||
subscribe: (listener: StateListener<State>) => () => void; | ||
}]; | ||
export {}; | ||
} | ||
export default function create<TState extends State>(createState: (set: SetState<State>, get: GetState<State>) => TState): [UseStore<TState>, StoreApi<TState>]; |
@@ -1,2 +0,2 @@ | ||
import { useCallback, useRef, useReducer, useLayoutEffect } from 'react'; | ||
import { useRef, useReducer, useLayoutEffect } from 'react'; | ||
@@ -32,14 +32,31 @@ function shallowEqual(objA, objB) { | ||
const setState = partialState => { | ||
state = Object.assign({}, state, typeof partialState === 'function' ? partialState(state) : partialState); | ||
listeners.forEach(listener => listener(state)); | ||
const setState = partial => { | ||
const partialState = typeof partial === 'function' ? partial(state) : partial; | ||
if (partialState !== state) { | ||
state = Object.assign({}, state, partialState); | ||
listeners.forEach(listener => listener(state)); | ||
} | ||
}; | ||
const getState = () => state; | ||
const getState = () => state; // Optional selector param goes first so we can infer its return type and use | ||
// it for listener | ||
const subscribe = listener => { | ||
const subscribe = (selectorOrListener, listenerOrUndef) => { | ||
let listener = selectorOrListener; // Existance of second param means a selector was passed in | ||
if (listenerOrUndef) { | ||
// We know selector is not type StateListener so it must be StateSelector | ||
const selector = selectorOrListener; | ||
let stateSlice = selector(state); | ||
listener = () => { | ||
const selectedSlice = selector(state); | ||
if (!shallowEqual(stateSlice, stateSlice = selectedSlice)) listenerOrUndef(stateSlice); | ||
}; | ||
} | ||
listeners.add(listener); | ||
return () => { | ||
listeners.delete(listener); | ||
}; | ||
return () => void listeners.delete(listener); | ||
}; | ||
@@ -49,34 +66,30 @@ | ||
listeners.clear(); | ||
state = {}; | ||
}; | ||
function useStore(selector, dependencies) { | ||
// State selector gets entire state if no selector was passed in | ||
const stateSelector = typeof selector === 'function' ? selector : getState; | ||
const selectState = useCallback(stateSelector, dependencies); | ||
const selectStateRef = useRef(selectState); | ||
let [stateSlice, dispatch] = useReducer(reducer, state, selectState); // Call new selector if it has changed | ||
const useStore = (selector, dependencies) => { | ||
const selectorRef = useRef(selector); | ||
const depsRef = useRef(dependencies); | ||
let [stateSlice, dispatch] = useReducer(reducer, state, // Optional third argument but required to not be 'undefined' | ||
selector); // Need to manually get state slice if selector has changed with no deps or | ||
// deps exist and have changed | ||
if (selectState !== selectStateRef.current) stateSlice = selectState(state); // Store in ref to enable updating without rerunning subscribe/unsubscribe | ||
if (selector && (!dependencies && selector !== selectorRef.current || dependencies && !shallowEqual(dependencies, depsRef.current))) { | ||
stateSlice = selector(state); | ||
} // Update refs synchronously after view has been updated | ||
const stateSliceRef = useRef(stateSlice); // Update refs only after view has been updated | ||
useLayoutEffect(() => { | ||
selectStateRef.current = selectState; | ||
stateSliceRef.current = stateSlice; | ||
}, [selectState, stateSlice]); // Subscribe/unsubscribe to the store only on mount/unmount | ||
selectorRef.current = selector; | ||
depsRef.current = dependencies; | ||
}, dependencies || [selector]); | ||
useLayoutEffect(() => { | ||
return subscribe(() => { | ||
// Use the last selector passed to useStore to get current state slice | ||
const selectedSlice = selectStateRef.current(state); // Shallow compare previous state slice with current and rerender only if changed | ||
if (!shallowEqual(stateSliceRef.current, selectedSlice)) dispatch(selectedSlice); | ||
}); | ||
}, []); | ||
return selector ? subscribe( // Truthy check because it might be possible to set selectorRef to | ||
() => selectorRef.current ? selectorRef.current(state) : state, dispatch) : subscribe(dispatch); // Only resubscribe to the store when changing selector from function to | ||
// undefined or undefined to function | ||
}, [!selector]); | ||
return stateSlice; | ||
} | ||
}; | ||
let state = createState(setState, getState); | ||
const api = { | ||
return [useStore, { | ||
destroy, | ||
@@ -86,6 +99,5 @@ getState, | ||
subscribe | ||
}; | ||
return [useStore, api]; | ||
}]; | ||
} | ||
export default create; |
{ | ||
"name": "zustand", | ||
"version": "0.1.3", | ||
"version": "0.2.0", | ||
"description": "🐻 Bear necessities for state management in React", | ||
@@ -5,0 +5,0 @@ "main": "dist/cjs/index.js", |
101
readme.md
@@ -5,2 +5,4 @@ <p align="center"> | ||
[![Build Status](https://travis-ci.org/react-spring/zustand.svg?branch=master)](https://travis-ci.org/react-spring/zustand) [![npm version](https://badge.fury.io/js/zustand.svg)](https://badge.fury.io/js/zustand) | ||
npm install zustand | ||
@@ -17,10 +19,10 @@ | ||
// Name your store anything you like, but remember, it's a hook! | ||
// You store is a hook! Name it as you like | ||
const [useStore] = create(set => ({ | ||
// Everything in here is your state | ||
count: 1, | ||
// You don't have to nest your actions, but makes it easier to fetch them later on | ||
actions: { | ||
// You don't have to nest your actions, but makes it easier to fetch them later on | ||
inc: () => set(state => ({ count: state.count + 1 })), // same semantics as setState | ||
dec: () => set(state => ({ count: state.count - 1 })), | ||
dec: () => set(state => ({ count: state.count - 1 })), // ... it *merges* state | ||
}, | ||
@@ -42,8 +44,8 @@ })) | ||
function Controls() { | ||
// "actions" isn't special, we just named it like that to fetch updaters easier | ||
const { inc, dec } = useStore(state => state.actions) | ||
// "actions" isn't special, in this case it makes fetching updaters easier | ||
const actions = useStore(state => state.actions) | ||
return ( | ||
<> | ||
<button onClick={inc}>up</button> | ||
<button onClick={dec}>down</button> | ||
<button onClick={actions.inc}>up</button> | ||
<button onClick={actions.dec}>down</button> | ||
</> | ||
@@ -61,3 +63,3 @@ ) | ||
```jsx | ||
const data = useStore() | ||
const state = useStore() | ||
``` | ||
@@ -70,3 +72,3 @@ | ||
```jsx | ||
const { name, age } = useStore(state => ({ name: state.name, age: state.age })) | ||
const { foo, bar } = useStore(state => ({ foo: state.foo, bar: state.bar })) | ||
``` | ||
@@ -77,4 +79,4 @@ | ||
```jsx | ||
const name = useStore(state => state.name) | ||
const age = useStore(state => state.age) | ||
const foo = useStore(state => state.foo) | ||
const bar = useStore(state => state.bar) | ||
``` | ||
@@ -91,2 +93,27 @@ | ||
## Memoizing selectors, optimizing performance | ||
Flux stores usually call the selector on every render-pass. Most of the time this isn't much of a problem, but when your selectors are computationally expensive, or when you know the component renders a lot (for instance react-motion calling it 60 times per second for animation purposes) you may want to optimize it. | ||
```js | ||
const foo = useStore(state => state.foo[props.id]) | ||
``` | ||
In this case the selector `state => state.foo[props.id]` will run on every state change, as well as every time the component renders. This isn't expensive at all, but let's optimize it for arguments sake. | ||
You can either pass a static reference: | ||
```js | ||
const fooSelector = useCallback(state => state.foo[props.id], [props.id]) | ||
const foo = useStore(fooSelector) | ||
``` | ||
Or an optional dependencies array to let Zustand know when the selector updates: | ||
```js | ||
const foo = useStore(state => state.foo[props.id], [props.id]) | ||
``` | ||
From there on your selector will only run when either state changes, or the selector itself. | ||
## Async actions | ||
@@ -154,8 +181,7 @@ | ||
const reducer = (state, { type, ...payload }) => { | ||
const reducer = (state, { type, by = 1 }) => { | ||
switch (type) { | ||
case types.increase: return { ...state, count: state.count + 1 } | ||
case types.decrease: return { ...state, count: state.count - 1 } | ||
case types.increase: return { count: state.count + by } | ||
case types.decrease: return { count: state.count - by } | ||
} | ||
return state | ||
} | ||
@@ -169,3 +195,3 @@ | ||
const dispatch = useStore(state => state.dispatch) | ||
dispatch({ type: types.increase }) | ||
dispatch({ type: types.increase, by: 2 }) | ||
``` | ||
@@ -181,10 +207,12 @@ | ||
// Getting fresh state | ||
const n = api.getState().n | ||
const num = api.getState().n | ||
// Listening to changes | ||
const unsub = api.subscribe(state => console.log(state.n)) | ||
// And with a selector | ||
const unsub2 = api.subscribe(state => state.n, n => console.log(n)) | ||
// Updating state, will trigger listeners | ||
api.setState({ n: 1 }) | ||
// Unsubscribing handler | ||
// Unsubscribing listener | ||
unsub() | ||
// Destroying the store | ||
// Destroying the store (removing all listeners) | ||
api.destroy() | ||
@@ -207,36 +235,1 @@ ``` | ||
``` | ||
## Memoizing selectors, performance concerns, etc. (this is just additional info) | ||
Zustand tries to be as performant as possible while still being flexible but there are limitations. This is an attempt to breakdown how Zustand works to enable better estimations of the computational cost. | ||
A component is always subscribed to the part of the store that the latest selector returned: | ||
```js | ||
const foo = useStore(state => state.foo) // subscribed only to state.foo | ||
``` | ||
The selector is called first to return the selected state and again on ANY modification to the store, even updates made to a different part of the store: | ||
```js | ||
const [useStore, { setState }] = create(() => ({ foo: 'foo', bar: 'bar' })) | ||
function ComponentFoo() { | ||
return useStore(state => state.foo) | ||
} | ||
function ComponentBar() { | ||
return useStore(state => state.bar) | ||
} | ||
setState({ bar: 'new bar' }) // All selectors are called but only ComponentBar renders again | ||
``` | ||
Zustand calls selectors to compare the selected state (the return value of the selector) with the previous selected state. An update is dispatched to the component if the new selected state is different. The comparison is done with a shallow equality check. The component will then render again with the new selected state. Zustand has to check if the selector is new during the re render because it can be changed at any time. If the selector is new, it's called and the return value is used instead of the selected state that was dispatched. | ||
It's best to use selectors that are not computationally expensive as they are called on every update to the store. You can also skip the additional call to the selector by extracting the selector and passing it in as a static reference: | ||
```js | ||
const fooSelector = state => state.foo | ||
const foo = useStore(fooSelector) // fooSelector only called on initialization and store updates | ||
``` | ||
You can also pass an optional dependencies array to let Zustand know when the selector updates: | ||
```js | ||
// selector only called on initialization, store updates, and props.key updates | ||
const part = useStore(state => state[props.key], [props.key]) | ||
``` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
18813
211
225
2