Comparing version 1.1.1 to 1.2.0
@@ -0,1 +1,13 @@ | ||
## 1.2.0 | ||
New experimental optimized updates (uses immer patches internally). To use, your state selections need to be made using paths - and make use of the new methods and components `useStoreStateOpt` and `<InjectStoreStateOpt>` respectively. | ||
Instead of passing a function, you now pass an array of path selections. The state returned will be an array of values per each state selection path. E.g: | ||
```ts | ||
const [isDarkMode] = useStoreStateOpt(UIStore, [["isDarkMode"]]) | ||
``` | ||
The performance benefits stem from Pullstate not having to run equality checks on the results of your selected state and then re-render your component accordingly, but instead looks at the immer update patches directly for which paths changed in your state and re-renders the listeners on those paths. | ||
## 1.1.0 | ||
@@ -2,0 +14,0 @@ |
import { useStoreState } from "./useStoreState"; | ||
import { useStoreStateOpt } from "./useStoreStateOpt"; | ||
import { Store, update } from "./Store"; | ||
@@ -8,2 +9,3 @@ import { InjectStoreState } from "./InjectStoreState"; | ||
import { EAsyncEndTags, TPullstateAsyncAction } from "./async-types"; | ||
export { useStoreState, update, Store, InjectStoreState, PullstateProvider, useStores, createPullstateCore, createAsyncAction, successResult, errorResult, EAsyncEndTags, IPullstateInstanceConsumable, InjectAsyncAction, EAsyncActionInjectType, TInjectAsyncActionProps, TPullstateAsyncAction }; | ||
import { InjectStoreStateOpt } from "./InjectStoreStateOpt"; | ||
export { useStoreState, useStoreStateOpt, update, Store, InjectStoreState, InjectStoreStateOpt, PullstateProvider, useStores, createPullstateCore, createAsyncAction, successResult, errorResult, EAsyncEndTags, IPullstateInstanceConsumable, InjectAsyncAction, EAsyncActionInjectType, TInjectAsyncActionProps, TPullstateAsyncAction, }; |
@@ -1,2 +0,2 @@ | ||
import React,{useState,useRef,useCallback,useEffect,useContext,useMemo}from'react';import produce$1 from'immer';const isEqual = require("fast-deep-equal"); | ||
import React,{useState,useRef,useEffect,useContext,useMemo}from'react';import produce$1 from'immer';const isEqual = require("fast-deep-equal"); | ||
function useStoreState(store, getSubState) { | ||
@@ -12,10 +12,9 @@ const [subState, setSubState] = useState(() => getSubState ? getSubState(store.getRawState()) : store.getRawState()); | ||
updateRef.current.getSubState = getSubState; | ||
const onStoreUpdate = useCallback(() => { | ||
const nextSubState = updateRef.current.getSubState ? updateRef.current.getSubState(store.getRawState()) : store.getRawState(); | ||
if (updateRef.current.shouldUpdate && !isEqual(updateRef.current.currentSubState, nextSubState)) { | ||
setSubState(nextSubState); | ||
} | ||
}, []); | ||
if (updateRef.current.onStoreUpdate === null) { | ||
updateRef.current.onStoreUpdate = onStoreUpdate; | ||
updateRef.current.onStoreUpdate = function onStoreUpdate() { | ||
const nextSubState = updateRef.current.getSubState ? updateRef.current.getSubState(store.getRawState()) : store.getRawState(); | ||
if (updateRef.current.shouldUpdate && !isEqual(updateRef.current.currentSubState, nextSubState)) { | ||
setSubState(nextSubState); | ||
} | ||
}; | ||
store._addUpdateListener(updateRef.current.onStoreUpdate); | ||
@@ -25,5 +24,39 @@ } | ||
updateRef.current.shouldUpdate = false; | ||
store._removeUpdateListener(onStoreUpdate); | ||
store._removeUpdateListener(updateRef.current.onStoreUpdate); | ||
}, []); | ||
return subState; | ||
}let updateListenerOrd = 0; | ||
function fastGet(obj, path) { | ||
return path.reduce((cur = obj, key) => { | ||
return cur[key]; | ||
}, undefined); | ||
} | ||
function getSubStateFromPaths(store, paths) { | ||
const state = store.getRawState(); | ||
const resp = []; | ||
for (const path of paths) { | ||
resp.push(fastGet(state, path)); | ||
} | ||
return resp; | ||
} | ||
function useStoreStateOpt(store, paths) { | ||
const [subState, setSubState] = useState(() => getSubStateFromPaths(store, paths)); | ||
const updateRef = useRef({ | ||
shouldUpdate: true, | ||
onStoreUpdate: null, | ||
currentSubState: null, | ||
ordKey: `_${updateListenerOrd++}`, | ||
}); | ||
updateRef.current.currentSubState = subState; | ||
if (updateRef.current.onStoreUpdate === null) { | ||
updateRef.current.onStoreUpdate = function onStoreUpdateOpt() { | ||
setSubState(getSubStateFromPaths(store, paths)); | ||
}; | ||
store._addUpdateListenerOpt(updateRef.current.onStoreUpdate, updateRef.current.ordKey, paths); | ||
} | ||
useEffect(() => () => { | ||
updateRef.current.shouldUpdate = false; | ||
store._removeUpdateListenerOpt(updateRef.current.ordKey); | ||
}, []); | ||
return subState; | ||
}const isEqual$1 = require("fast-deep-equal"); | ||
@@ -57,2 +90,3 @@ const Immer = require("immer"); | ||
} | ||
const optPathDivider = "~._.~"; | ||
class Store { | ||
@@ -65,2 +99,6 @@ constructor(initialState) { | ||
this.reactionCreators = []; | ||
this.optimizedUpdateListeners = {}; | ||
this.optimizedUpdateListenerPaths = {}; | ||
this.optimizedListenerPropertyMap = {}; | ||
this._optListenerCount = 0; | ||
this.currentState = initialState; | ||
@@ -86,3 +124,3 @@ this.initialState = initialState; | ||
} | ||
_updateState(nextState) { | ||
_updateState(nextState, updateKeyedPaths = []) { | ||
this.currentState = nextState; | ||
@@ -97,2 +135,15 @@ for (const runReaction of this.reactions) { | ||
this.updateListeners.forEach(listener => listener()); | ||
if (updateKeyedPaths.length > 0) { | ||
const updateOrds = new Set(); | ||
for (const keyedPath of updateKeyedPaths) { | ||
if (this.optimizedListenerPropertyMap[keyedPath]) { | ||
for (const ord of this.optimizedListenerPropertyMap[keyedPath]) { | ||
updateOrds.add(ord); | ||
} | ||
} | ||
} | ||
for (const ord of updateOrds.values()) { | ||
this.optimizedUpdateListeners[ord](); | ||
} | ||
} | ||
} | ||
@@ -103,5 +154,28 @@ } | ||
} | ||
_addUpdateListenerOpt(listener, ordKey, paths) { | ||
this.optimizedUpdateListeners[ordKey] = listener; | ||
const listenerPathsKeyed = paths.map(path => path.join(optPathDivider)); | ||
this.optimizedUpdateListenerPaths[ordKey] = listenerPathsKeyed; | ||
for (const keyedPath of listenerPathsKeyed) { | ||
if (this.optimizedListenerPropertyMap[keyedPath] == null) { | ||
this.optimizedListenerPropertyMap[keyedPath] = [ordKey]; | ||
} | ||
else { | ||
this.optimizedListenerPropertyMap[keyedPath].push(ordKey); | ||
} | ||
} | ||
this._optListenerCount++; | ||
} | ||
_removeUpdateListener(listener) { | ||
this.updateListeners = this.updateListeners.filter(f => f !== listener); | ||
} | ||
_removeUpdateListenerOpt(ordKey) { | ||
const listenerPathsKeyed = this.optimizedUpdateListenerPaths[ordKey]; | ||
for (const keyedPath of listenerPathsKeyed) { | ||
this.optimizedListenerPropertyMap[keyedPath] = this.optimizedListenerPropertyMap[keyedPath].filter(ord => ord !== ordKey); | ||
} | ||
delete this.optimizedUpdateListenerPaths[ordKey]; | ||
delete this.optimizedUpdateListeners[ordKey]; | ||
this._optListenerCount--; | ||
} | ||
subscribe(watch, listener) { | ||
@@ -150,6 +224,33 @@ if (!this.ssr) { | ||
const currentState = store.getRawState(); | ||
const nextState = produce(currentState, s => updater(s, currentState), patchesCallback); | ||
if (nextState !== currentState) { | ||
store._updateState(nextState); | ||
if (store._optListenerCount > 0) { | ||
let changePatches; | ||
const nextState = produce(currentState, s => updater(s, currentState), (patches, inversePatches) => { | ||
if (patchesCallback) { | ||
patchesCallback(patches, inversePatches); | ||
} | ||
changePatches = patches; | ||
}); | ||
if (changePatches.length > 0) { | ||
const updateKeyedPathsMap = {}; | ||
for (const patch of changePatches) { | ||
let curKey; | ||
for (const p of patch.path) { | ||
if (curKey) { | ||
curKey = `${curKey}${optPathDivider}${p}`; | ||
} | ||
else { | ||
curKey = p; | ||
} | ||
updateKeyedPathsMap[curKey] = true; | ||
} | ||
} | ||
store._updateState(nextState, Object.keys(updateKeyedPathsMap)); | ||
} | ||
} | ||
else { | ||
const nextState = produce(currentState, s => updater(s, currentState), patchesCallback); | ||
if (nextState !== currentState) { | ||
store._updateState(nextState); | ||
} | ||
} | ||
}function InjectStoreState({ store, on = s => s, children, }) { | ||
@@ -724,2 +825,5 @@ const state = useStoreState(store, on); | ||
return props.children(response); | ||
}export{useStoreState,update,Store,InjectStoreState,PullstateProvider,useStores,createPullstateCore,createAsyncAction,successResult,errorResult,EAsyncEndTags,InjectAsyncAction,EAsyncActionInjectType}; | ||
}function InjectStoreStateOpt({ store, paths, children, }) { | ||
const state = useStoreStateOpt(store, paths); | ||
return children(state); | ||
}export{useStoreState,useStoreStateOpt,update,Store,InjectStoreState,InjectStoreStateOpt,PullstateProvider,useStores,createPullstateCore,createAsyncAction,successResult,errorResult,EAsyncEndTags,InjectAsyncAction,EAsyncActionInjectType}; |
@@ -12,10 +12,9 @@ 'use strict';Object.defineProperty(exports,'__esModule',{value:true});function _interopDefault(e){return(e&&(typeof e==='object')&&'default'in e)?e['default']:e}var React=require('react'),React__default=_interopDefault(React),produce$1=_interopDefault(require('immer'));const isEqual = require("fast-deep-equal"); | ||
updateRef.current.getSubState = getSubState; | ||
const onStoreUpdate = React.useCallback(() => { | ||
const nextSubState = updateRef.current.getSubState ? updateRef.current.getSubState(store.getRawState()) : store.getRawState(); | ||
if (updateRef.current.shouldUpdate && !isEqual(updateRef.current.currentSubState, nextSubState)) { | ||
setSubState(nextSubState); | ||
} | ||
}, []); | ||
if (updateRef.current.onStoreUpdate === null) { | ||
updateRef.current.onStoreUpdate = onStoreUpdate; | ||
updateRef.current.onStoreUpdate = function onStoreUpdate() { | ||
const nextSubState = updateRef.current.getSubState ? updateRef.current.getSubState(store.getRawState()) : store.getRawState(); | ||
if (updateRef.current.shouldUpdate && !isEqual(updateRef.current.currentSubState, nextSubState)) { | ||
setSubState(nextSubState); | ||
} | ||
}; | ||
store._addUpdateListener(updateRef.current.onStoreUpdate); | ||
@@ -25,5 +24,39 @@ } | ||
updateRef.current.shouldUpdate = false; | ||
store._removeUpdateListener(onStoreUpdate); | ||
store._removeUpdateListener(updateRef.current.onStoreUpdate); | ||
}, []); | ||
return subState; | ||
}let updateListenerOrd = 0; | ||
function fastGet(obj, path) { | ||
return path.reduce((cur = obj, key) => { | ||
return cur[key]; | ||
}, undefined); | ||
} | ||
function getSubStateFromPaths(store, paths) { | ||
const state = store.getRawState(); | ||
const resp = []; | ||
for (const path of paths) { | ||
resp.push(fastGet(state, path)); | ||
} | ||
return resp; | ||
} | ||
function useStoreStateOpt(store, paths) { | ||
const [subState, setSubState] = React.useState(() => getSubStateFromPaths(store, paths)); | ||
const updateRef = React.useRef({ | ||
shouldUpdate: true, | ||
onStoreUpdate: null, | ||
currentSubState: null, | ||
ordKey: `_${updateListenerOrd++}`, | ||
}); | ||
updateRef.current.currentSubState = subState; | ||
if (updateRef.current.onStoreUpdate === null) { | ||
updateRef.current.onStoreUpdate = function onStoreUpdateOpt() { | ||
setSubState(getSubStateFromPaths(store, paths)); | ||
}; | ||
store._addUpdateListenerOpt(updateRef.current.onStoreUpdate, updateRef.current.ordKey, paths); | ||
} | ||
React.useEffect(() => () => { | ||
updateRef.current.shouldUpdate = false; | ||
store._removeUpdateListenerOpt(updateRef.current.ordKey); | ||
}, []); | ||
return subState; | ||
}const isEqual$1 = require("fast-deep-equal"); | ||
@@ -57,2 +90,3 @@ const Immer = require("immer"); | ||
} | ||
const optPathDivider = "~._.~"; | ||
class Store { | ||
@@ -65,2 +99,6 @@ constructor(initialState) { | ||
this.reactionCreators = []; | ||
this.optimizedUpdateListeners = {}; | ||
this.optimizedUpdateListenerPaths = {}; | ||
this.optimizedListenerPropertyMap = {}; | ||
this._optListenerCount = 0; | ||
this.currentState = initialState; | ||
@@ -86,3 +124,3 @@ this.initialState = initialState; | ||
} | ||
_updateState(nextState) { | ||
_updateState(nextState, updateKeyedPaths = []) { | ||
this.currentState = nextState; | ||
@@ -97,2 +135,15 @@ for (const runReaction of this.reactions) { | ||
this.updateListeners.forEach(listener => listener()); | ||
if (updateKeyedPaths.length > 0) { | ||
const updateOrds = new Set(); | ||
for (const keyedPath of updateKeyedPaths) { | ||
if (this.optimizedListenerPropertyMap[keyedPath]) { | ||
for (const ord of this.optimizedListenerPropertyMap[keyedPath]) { | ||
updateOrds.add(ord); | ||
} | ||
} | ||
} | ||
for (const ord of updateOrds.values()) { | ||
this.optimizedUpdateListeners[ord](); | ||
} | ||
} | ||
} | ||
@@ -103,5 +154,28 @@ } | ||
} | ||
_addUpdateListenerOpt(listener, ordKey, paths) { | ||
this.optimizedUpdateListeners[ordKey] = listener; | ||
const listenerPathsKeyed = paths.map(path => path.join(optPathDivider)); | ||
this.optimizedUpdateListenerPaths[ordKey] = listenerPathsKeyed; | ||
for (const keyedPath of listenerPathsKeyed) { | ||
if (this.optimizedListenerPropertyMap[keyedPath] == null) { | ||
this.optimizedListenerPropertyMap[keyedPath] = [ordKey]; | ||
} | ||
else { | ||
this.optimizedListenerPropertyMap[keyedPath].push(ordKey); | ||
} | ||
} | ||
this._optListenerCount++; | ||
} | ||
_removeUpdateListener(listener) { | ||
this.updateListeners = this.updateListeners.filter(f => f !== listener); | ||
} | ||
_removeUpdateListenerOpt(ordKey) { | ||
const listenerPathsKeyed = this.optimizedUpdateListenerPaths[ordKey]; | ||
for (const keyedPath of listenerPathsKeyed) { | ||
this.optimizedListenerPropertyMap[keyedPath] = this.optimizedListenerPropertyMap[keyedPath].filter(ord => ord !== ordKey); | ||
} | ||
delete this.optimizedUpdateListenerPaths[ordKey]; | ||
delete this.optimizedUpdateListeners[ordKey]; | ||
this._optListenerCount--; | ||
} | ||
subscribe(watch, listener) { | ||
@@ -150,6 +224,33 @@ if (!this.ssr) { | ||
const currentState = store.getRawState(); | ||
const nextState = produce(currentState, s => updater(s, currentState), patchesCallback); | ||
if (nextState !== currentState) { | ||
store._updateState(nextState); | ||
if (store._optListenerCount > 0) { | ||
let changePatches; | ||
const nextState = produce(currentState, s => updater(s, currentState), (patches, inversePatches) => { | ||
if (patchesCallback) { | ||
patchesCallback(patches, inversePatches); | ||
} | ||
changePatches = patches; | ||
}); | ||
if (changePatches.length > 0) { | ||
const updateKeyedPathsMap = {}; | ||
for (const patch of changePatches) { | ||
let curKey; | ||
for (const p of patch.path) { | ||
if (curKey) { | ||
curKey = `${curKey}${optPathDivider}${p}`; | ||
} | ||
else { | ||
curKey = p; | ||
} | ||
updateKeyedPathsMap[curKey] = true; | ||
} | ||
} | ||
store._updateState(nextState, Object.keys(updateKeyedPathsMap)); | ||
} | ||
} | ||
else { | ||
const nextState = produce(currentState, s => updater(s, currentState), patchesCallback); | ||
if (nextState !== currentState) { | ||
store._updateState(nextState); | ||
} | ||
} | ||
}function InjectStoreState({ store, on = s => s, children, }) { | ||
@@ -722,2 +823,5 @@ const state = useStoreState(store, on); | ||
return props.children(response); | ||
}exports.useStoreState=useStoreState;exports.update=update;exports.Store=Store;exports.InjectStoreState=InjectStoreState;exports.PullstateProvider=PullstateProvider;exports.useStores=useStores;exports.createPullstateCore=createPullstateCore;exports.createAsyncAction=createAsyncAction;exports.successResult=successResult;exports.errorResult=errorResult;exports.InjectAsyncAction=InjectAsyncAction; | ||
}function InjectStoreStateOpt({ store, paths, children, }) { | ||
const state = useStoreStateOpt(store, paths); | ||
return children(state); | ||
}exports.useStoreState=useStoreState;exports.useStoreStateOpt=useStoreStateOpt;exports.update=update;exports.Store=Store;exports.InjectStoreState=InjectStoreState;exports.InjectStoreStateOpt=InjectStoreStateOpt;exports.PullstateProvider=PullstateProvider;exports.useStores=useStores;exports.createPullstateCore=createPullstateCore;exports.createAsyncAction=createAsyncAction;exports.successResult=successResult;exports.errorResult=errorResult;exports.InjectAsyncAction=InjectAsyncAction; |
import { Patch } from "immer"; | ||
import { TPath } from "./useStoreStateOpt"; | ||
export declare type TPullstateUpdateListener = () => void; | ||
@@ -19,2 +20,6 @@ export interface IStoreInternalOptions<S> { | ||
private reactionCreators; | ||
private optimizedUpdateListeners; | ||
private optimizedUpdateListenerPaths; | ||
private optimizedListenerPropertyMap; | ||
_optListenerCount: number; | ||
constructor(initialState: S); | ||
@@ -26,5 +31,7 @@ _setInternalOptions({ ssr, reactionCreators }: IStoreInternalOptions<S>): void; | ||
_updateStateWithoutReaction(nextState: S): void; | ||
_updateState(nextState: S): void; | ||
_updateState(nextState: S, updateKeyedPaths?: string[]): void; | ||
_addUpdateListener(listener: TPullstateUpdateListener): void; | ||
_addUpdateListenerOpt(listener: TPullstateUpdateListener, ordKey: string, paths: TPath[]): void; | ||
_removeUpdateListener(listener: TPullstateUpdateListener): void; | ||
_removeUpdateListenerOpt(ordKey: string): void; | ||
subscribe<T>(watch: (state: S) => T, listener: (watched: T, allState: S, previousWatched: T) => void): () => void; | ||
@@ -31,0 +38,0 @@ createReaction<T>(watch: (state: S) => T, reaction: TReactionFunction<S, T>): () => void; |
import { Store } from "./Store"; | ||
export interface IUpdateRef { | ||
shouldUpdate: boolean; | ||
onStoreUpdate: () => void; | ||
getSubState: any; | ||
currentSubState: any; | ||
} | ||
declare function useStoreState<S = any>(store: Store<S>): S; | ||
declare function useStoreState<S = any, SS = any>(store: Store<S>, getSubState: (state: S) => SS): SS; | ||
export { useStoreState }; |
{ | ||
"name": "pullstate", | ||
"version": "1.1.1", | ||
"version": "1.2.0", | ||
"description": "Simple state stores using immer and React hooks", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
96237
16
1933