From 2ae57e4e6b44706d5eecfeb238518027d5b60b4e Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sun, 1 Sep 2019 11:24:23 -0300 Subject: [PATCH 01/19] create memoization for simplest case --- src/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.js b/src/index.js index 8482f4f..2cdb616 100644 --- a/src/index.js +++ b/src/index.js @@ -80,6 +80,10 @@ export function createStore(name, state = {}, reducer=defaultReducer) { state, reducer, setState(action, callback) { + if (this.reducer === defaultReducer && action === this.state && typeof action !== 'object') { + if (typeof callback === 'function') callback(this.state) + return; + } this.state = this.reducer(this.state, action); this.setters.forEach(setter => setter(this.state)); if (subscriptions[name].length) { From 7cfb3bcd8ebfde9c4773e5223e70f8e2a0e894d1 Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sun, 1 Sep 2019 12:15:20 -0300 Subject: [PATCH 02/19] adding mapDependency schema --- src/index.js | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/index.js b/src/index.js index 2cdb616..4f9261d 100644 --- a/src/index.js +++ b/src/index.js @@ -79,19 +79,29 @@ export function createStore(name, state = {}, reducer=defaultReducer) { const store = { state, reducer, - setState(action, callback) { + setState(mapDependency, action, callback) { if (this.reducer === defaultReducer && action === this.state && typeof action !== 'object') { if (typeof callback === 'function') callback(this.state) return; } - this.state = this.reducer(this.state, action); - this.setters.forEach(setter => setter(this.state)); + const currentState = this.state; + const newState = this.reducer(this.state, action); + const prevResult = mapDependency(currentState); + const newResult = mapDependency(newState); + if (prevResult === newResult) { + if (typeof callback === 'function') callback(this.state); + return; + } + this.state = newState; + for (let setter of this.setters.get(mapDependency)) { + setter(this.state); + } if (subscriptions[name].length) { subscriptions[name].forEach(c => c(this.state, action)); } if (typeof callback === 'function') callback(this.state) }, - setters: [] + setters: new Map(), }; store.setState = store.setState.bind(store); subscriptions[name] = []; @@ -115,25 +125,39 @@ export function getStoreByName(name) { } } +const defaultMapDependencies = (state) => state; + /** * Returns a [ state, setState ] pair for the selected store. Can only be called within React Components * @param {String|StoreInterface} identifier - The identifier for the wanted store * @returns {Array} the [state, setState] pair. */ -export function useStore(identifier) { +export function useStore(identifier, mapDependency=defaultMapDependencies) { const store = getStoreByIdentifier(identifier); + if (!store) { + throw 'store does not exist'; + } + if (typeof mapDependency !== 'function') { + throw 'dependencyMap must be a function'; + } const [ state, set ] = useState(store.state); + if (!store.setters.get(mapDependency)) { + store.setters.set(mapDependency, new Set()); + } + + const setters = store.setters.get(mapDependency); + useEffect(() => { - if (!store.setters.includes(set)) { - store.setters.push(set); + if (!setters.has(set)) { + setters.add(set); } return () => { - store.setters = store.setters.filter(setter => setter !== set) + setters.remove(set); } }, []) - return [ state, store.setState ]; + return [ state, (...args) => store.setState(mapDependency, ...args) ]; } From 9dc43836143f77b697a2ba23b82dfce147fda40a Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sun, 1 Sep 2019 12:24:16 -0300 Subject: [PATCH 03/19] initialising setters and exposing setState correctly --- src/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index 4f9261d..d362e13 100644 --- a/src/index.js +++ b/src/index.js @@ -4,13 +4,15 @@ let stores = {}; let subscriptions = {}; const defaultReducer = (state, payload) => payload; +const defaultMapDependencies = (state) => state; /** The public interface of a store */ class StoreInterface { constructor(name, store, useReducer) { this.name = name; + const setState = store.setState.bind(store, defaultMapDependencies); useReducer ? - this.dispatch = store.setState : this.setState = store.setState; + this.dispatch = setState : this.setState = setState; this.getState = () => store.state; this.subscribe = this.subscribe.bind(this); } @@ -105,8 +107,8 @@ export function createStore(name, state = {}, reducer=defaultReducer) { }; store.setState = store.setState.bind(store); subscriptions[name] = []; + store.setters.set(defaultMapDependencies, new Set()) store.public = new StoreInterface(name, store, reducer !== defaultReducer); - stores = Object.assign({}, stores, { [name]: store }); return store.public; } @@ -125,8 +127,6 @@ export function getStoreByName(name) { } } -const defaultMapDependencies = (state) => state; - /** * Returns a [ state, setState ] pair for the selected store. Can only be called within React Components * @param {String|StoreInterface} identifier - The identifier for the wanted store @@ -155,7 +155,7 @@ export function useStore(identifier, mapDependency=defaultMapDependencies) { } return () => { - setters.remove(set); + setters.delete(set); } }, []) From a3cdeb3e348173e2aa7b73aed6d30cd7c32d0a3e Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sun, 1 Sep 2019 13:10:26 -0300 Subject: [PATCH 04/19] run upate check on all setters --- src/index.js | 48 +++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/index.js b/src/index.js index d362e13..88dd99b 100644 --- a/src/index.js +++ b/src/index.js @@ -10,9 +10,8 @@ const defaultMapDependencies = (state) => state; class StoreInterface { constructor(name, store, useReducer) { this.name = name; - const setState = store.setState.bind(store, defaultMapDependencies); useReducer ? - this.dispatch = setState : this.setState = setState; + this.dispatch = store.setState : this.setState = store.setState; this.getState = () => store.state; this.subscribe = this.subscribe.bind(this); } @@ -81,23 +80,31 @@ export function createStore(name, state = {}, reducer=defaultReducer) { const store = { state, reducer, - setState(mapDependency, action, callback) { + setState(action, callback) { if (this.reducer === defaultReducer && action === this.state && typeof action !== 'object') { + console.log('basic memoization, not updating'); if (typeof callback === 'function') callback(this.state) return; } const currentState = this.state; const newState = this.reducer(this.state, action); - const prevResult = mapDependency(currentState); - const newResult = mapDependency(newState); - if (prevResult === newResult) { - if (typeof callback === 'function') callback(this.state); - return; - } this.state = newState; - for (let setter of this.setters.get(mapDependency)) { - setter(this.state); - } + console.log(this.setters); + this.setters.forEach((_, mapDependency) => { + if (this.setters.get(mapDependency).length) { + return; + } + const prevResult = mapDependency(currentState); + const newResult = mapDependency(newState); + if (prevResult === newResult) { + console.log('advanced memoization, not updating'); + return; + } + for (let setter of this.setters.get(mapDependency)) { + setter(this.state); + console.log('updating'); + } + }); if (subscriptions[name].length) { subscriptions[name].forEach(c => c(this.state, action)); } @@ -143,21 +150,24 @@ export function useStore(identifier, mapDependency=defaultMapDependencies) { const [ state, set ] = useState(store.state); - if (!store.setters.get(mapDependency)) { - store.setters.set(mapDependency, new Set()); - } - - const setters = store.setters.get(mapDependency); - useEffect(() => { + if (!store.setters.get(mapDependency)) { + store.setters.set(mapDependency, new Set()); + } + + const setters = store.setters.get(mapDependency); + if (!setters.has(set)) { setters.add(set); } return () => { setters.delete(set); + if (!setters.length) { + store.setters.delete(mapDependency); + } } }, []) - return [ state, (...args) => store.setState(mapDependency, ...args) ]; + return [ state, store.setState]; } From 0f391b4a29a917866f6c33bed098f116ab2dd746 Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sat, 8 Feb 2020 16:02:47 -0300 Subject: [PATCH 05/19] improving readability --- example/basic.js | 23 ++++++++++++++++++----- src/index.js | 32 ++++++++++++++++---------------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/example/basic.js b/example/basic.js index 58fc386..dafc5e0 100644 --- a/example/basic.js +++ b/example/basic.js @@ -5,18 +5,31 @@ const defaultStyles = { padding: 10, backgroundColor: 'navy', marginTop: 10, color: 'white' } +const mapStateToProps = (state) => state.number; + // Creating a nameless store, do that if you do not wish to have multiple stores in your app -createStore('clickCounter', 0); +createStore('clickCounter', { number: 0 }, (state, number) => { + return { number } +}); + +let delay = 0; +function setDelay() { + delay += 0.5; + if (delay > 1) { + delay = 0; + } + return delay; +} export function StatefulHello() { // just use the useStore method to grab the state and the setState methods - const [ state, setState ] = useStore('clickCounter'); + const [ state, setState ] = useStore('clickCounter', mapStateToProps); return (

Hello, component!

-

The button inside this component was clicked {state} times

- +

The button inside this component was clicked {state.number} times

+
); } @@ -27,7 +40,7 @@ export function AnotherComponent() { return (

Hello, this is a second component, with no relation to the one on the top

-

But it is still aware of how many times the button was clicked: {value}

+

But it is still aware of how many times the button was clicked: {value.number}

) } diff --git a/src/index.js b/src/index.js index 88dd99b..6f6203e 100644 --- a/src/index.js +++ b/src/index.js @@ -86,35 +86,35 @@ export function createStore(name, state = {}, reducer=defaultReducer) { if (typeof callback === 'function') callback(this.state) return; } + const currentState = this.state; const newState = this.reducer(this.state, action); this.state = newState; - console.log(this.setters); - this.setters.forEach((_, mapDependency) => { - if (this.setters.get(mapDependency).length) { - return; - } + + this.settersPerDependency.forEach((setters, mapDependency) => { const prevResult = mapDependency(currentState); const newResult = mapDependency(newState); if (prevResult === newResult) { console.log('advanced memoization, not updating'); return; } - for (let setter of this.setters.get(mapDependency)) { + for (let setter of setters) { setter(this.state); console.log('updating'); } }); + if (subscriptions[name].length) { subscriptions[name].forEach(c => c(this.state, action)); } + if (typeof callback === 'function') callback(this.state) }, - setters: new Map(), + settersPerDependency: new Map(), }; store.setState = store.setState.bind(store); subscriptions[name] = []; - store.setters.set(defaultMapDependencies, new Set()) + store.settersPerDependency.set(defaultMapDependencies, new Set()) store.public = new StoreInterface(name, store, reducer !== defaultReducer); stores = Object.assign({}, stores, { [name]: store }); return store.public; @@ -151,20 +151,20 @@ export function useStore(identifier, mapDependency=defaultMapDependencies) { const [ state, set ] = useState(store.state); useEffect(() => { - if (!store.setters.get(mapDependency)) { - store.setters.set(mapDependency, new Set()); + if (!store.settersPerDependency.has(mapDependency)) { + store.settersPerDependency.set(mapDependency, new Set()); } - const setters = store.setters.get(mapDependency); + const settersPerDependency = store.settersPerDependency.get(mapDependency); - if (!setters.has(set)) { - setters.add(set); + if (!settersPerDependency.has(set)) { + settersPerDependency.add(set); } return () => { - setters.delete(set); - if (!setters.length) { - store.setters.delete(mapDependency); + settersPerDependency.delete(set); + if (!settersPerDependency.size) { + store.settersPerDependency.delete(mapDependency); } } }, []) From 5e9b8ad716f996924bbe8aeac786bae15bfc0a55 Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sat, 8 Feb 2020 16:03:29 -0300 Subject: [PATCH 06/19] improving readability --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 6f6203e..cfdd5bd 100644 --- a/src/index.js +++ b/src/index.js @@ -86,7 +86,7 @@ export function createStore(name, state = {}, reducer=defaultReducer) { if (typeof callback === 'function') callback(this.state) return; } - + const currentState = this.state; const newState = this.reducer(this.state, action); this.state = newState; From c2af89a90fc985f410a23f1b6c8ee23001f2a6ce Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sat, 8 Feb 2020 17:55:51 -0300 Subject: [PATCH 07/19] adding memoization tests, improving code readbility --- src/index.js | 58 ++++--- src/tests/memoization.test.js | 276 ++++++++++++++++++++++++++++++++++ 2 files changed, 310 insertions(+), 24 deletions(-) create mode 100644 src/tests/memoization.test.js diff --git a/src/index.js b/src/index.js index cfdd5bd..da2ff7b 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,7 @@ let stores = {}; let subscriptions = {}; const defaultReducer = (state, payload) => payload; -const defaultMapDependencies = (state) => state; +const defaultMemoFn = (state) => state; /** The public interface of a store */ class StoreInterface { @@ -81,8 +81,12 @@ export function createStore(name, state = {}, reducer=defaultReducer) { state, reducer, setState(action, callback) { - if (this.reducer === defaultReducer && action === this.state && typeof action !== 'object') { - console.log('basic memoization, not updating'); + const isPrimitiveStateWithoutReducerAndIsPreviousState = + this.reducer === defaultReducer + && action === this.state + && typeof action !== 'object'; + + if (isPrimitiveStateWithoutReducerAndIsPreviousState) { if (typeof callback === 'function') callback(this.state) return; } @@ -91,16 +95,14 @@ export function createStore(name, state = {}, reducer=defaultReducer) { const newState = this.reducer(this.state, action); this.state = newState; - this.settersPerDependency.forEach((setters, mapDependency) => { - const prevResult = mapDependency(currentState); - const newResult = mapDependency(newState); + this.updatersPerMemoFunction.forEach((updaters, memoFn) => { + const prevResult = memoFn(currentState); + const newResult = memoFn(newState); if (prevResult === newResult) { - console.log('advanced memoization, not updating'); return; } - for (let setter of setters) { - setter(this.state); - console.log('updating'); + for (let updateComponent of updaters) { + updateComponent(this.state); } }); @@ -110,13 +112,15 @@ export function createStore(name, state = {}, reducer=defaultReducer) { if (typeof callback === 'function') callback(this.state) }, - settersPerDependency: new Map(), + updatersPerMemoFunction: new Map(), }; + store.setState = store.setState.bind(store); + store.updatersPerMemoFunction.set(defaultMemoFn, new Set()) + stores = Object.assign({}, stores, { [name]: store }); subscriptions[name] = []; - store.settersPerDependency.set(defaultMapDependencies, new Set()) + store.public = new StoreInterface(name, store, reducer !== defaultReducer); - stores = Object.assign({}, stores, { [name]: store }); return store.public; } @@ -137,34 +141,40 @@ export function getStoreByName(name) { /** * Returns a [ state, setState ] pair for the selected store. Can only be called within React Components * @param {String|StoreInterface} identifier - The identifier for the wanted store + * @callback memoFn [state => state] - A memoization function to optimize component rerender. Receive the store state and return a subset of it. The component will only rerender when the subset changes. * @returns {Array} the [state, setState] pair. */ -export function useStore(identifier, mapDependency=defaultMapDependencies) { + +/** + * + * @param {memoFn} state + */ +export function useStore(identifier, memoFn=defaultMemoFn) { const store = getStoreByIdentifier(identifier); if (!store) { throw 'store does not exist'; } - if (typeof mapDependency !== 'function') { - throw 'dependencyMap must be a function'; + if (typeof memoFn !== 'function') { + throw 'memoFn must be a function'; } const [ state, set ] = useState(store.state); useEffect(() => { - if (!store.settersPerDependency.has(mapDependency)) { - store.settersPerDependency.set(mapDependency, new Set()); + if (!store.updatersPerMemoFunction.has(memoFn)) { + store.updatersPerMemoFunction.set(memoFn, new Set()); } - const settersPerDependency = store.settersPerDependency.get(mapDependency); + const updatersPerMemoFunction = store.updatersPerMemoFunction.get(memoFn); - if (!settersPerDependency.has(set)) { - settersPerDependency.add(set); + if (!updatersPerMemoFunction.has(set)) { + updatersPerMemoFunction.add(set); } return () => { - settersPerDependency.delete(set); - if (!settersPerDependency.size) { - store.settersPerDependency.delete(mapDependency); + updatersPerMemoFunction.delete(set); + if (!updatersPerMemoFunction.size) { + store.updatersPerMemoFunction.delete(memoFn); } } }, []) diff --git a/src/tests/memoization.test.js b/src/tests/memoization.test.js new file mode 100644 index 0000000..2372bb6 --- /dev/null +++ b/src/tests/memoization.test.js @@ -0,0 +1,276 @@ +import React, { useEffect } from 'react'; +import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { createStore, useStore } from '..'; + +describe('store memoization', () => { + it('Should not update components if setState is called with the same previous state', (done) => { + const store = createStore('store', 0); + const componentRenderCount = jest.fn(); + const anotherComponentRenderCount = jest.fn(); + + const Component = (props) => { + const [state, setState] = useStore(store); + + componentRenderCount(); + + return ( + + ); + } + + const AnotherComponent = (props) => { + const [state] = useStore(store); + + anotherComponentRenderCount(); + + return ( +
+ {state} +
+ ); + } + + const renderedComponent = mount(); + const renderedAnotherComponent = mount(); + + requestAnimationFrame(() => { + expect(componentRenderCount).toHaveBeenCalledTimes(1); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(1); + + renderedComponent.find('button').simulate('click'); + expect(componentRenderCount).toHaveBeenCalledTimes(2); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2); + + act(() => store.setState(3)); + expect(componentRenderCount).toHaveBeenCalledTimes(3); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3); + + act(() => store.setState(3)); + expect(componentRenderCount).toHaveBeenCalledTimes(3); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3); + + act(() => store.setState(3)); + expect(componentRenderCount).toHaveBeenCalledTimes(3); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3); + + act(() => store.setState(1)); + expect(componentRenderCount).toHaveBeenCalledTimes(4); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(4); + + act(() => store.setState(1)); + expect(componentRenderCount).toHaveBeenCalledTimes(4); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(4); + + expect(renderedComponent.text()).toBe('This button has been clicked 1 times') + expect(renderedAnotherComponent.text()).toBe('1') + + done(); + }) + }); + + it('Should always update components if state is complex and no mapDeps is provided', (done) => { + const store = createStore('complexState', { number: 0 }); + const componentRenderCount = jest.fn(); + const anotherComponentRenderCount = jest.fn(); + + const Component = (props) => { + const [state, setState] = useStore(store); + + componentRenderCount(); + + return ( + + ); + } + + const AnotherComponent = (props) => { + const [state] = useStore(store); + + anotherComponentRenderCount(); + + return ( +
+ {state.number} +
+ ); + } + + const renderedComponent = mount(); + const renderedAnotherComponent = mount(); + + requestAnimationFrame(() => { + expect(componentRenderCount).toHaveBeenCalledTimes(1); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(1); + + renderedComponent.find('button').simulate('click'); + expect(componentRenderCount).toHaveBeenCalledTimes(2); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2); + + act(() => store.setState({ number: 3 })); + expect(componentRenderCount).toHaveBeenCalledTimes(3); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3); + + act(() => store.setState({ number: 3 })); + expect(componentRenderCount).toHaveBeenCalledTimes(4); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(4); + + act(() => store.setState({ number: 3 })); + expect(componentRenderCount).toHaveBeenCalledTimes(5); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(5); + + act(() => store.setState({ number: 1 })); + expect(componentRenderCount).toHaveBeenCalledTimes(6); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(6); + + act(() => store.setState({ number: 1 })); + expect(componentRenderCount).toHaveBeenCalledTimes(7); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(7); + + expect(renderedComponent.text()).toBe('This button has been clicked 1 times') + expect(renderedAnotherComponent.text()).toBe('1') + + done(); + }) + }); + + it('Should only update component if the field it wants is updated', (done) => { + const store = createStore('complexStore2', {foo: { bar: 2 }, baz: 3}); + + const componentRenderCount = jest.fn(); + const anotherComponentRenderCount = jest.fn(); + + const memoBar = (state) => state.foo && state.foo.bar; + const memoBaz = (state) => state.baz; + + const Component = (props) => { + const [state] = useStore(store, memoBar); + + componentRenderCount(); + + return ( +
+ {state.foo && state.foo.bar} +
+ ); + } + + const AnotherComponent = (props) => { + const [state] = useStore(store, memoBaz); + + anotherComponentRenderCount(); + + return ( +
+ {state.baz} +
+ ); + } + + const renderedComponent = mount(); + const renderedAnotherComponent = mount(); + + requestAnimationFrame(() => { + expect(componentRenderCount).toHaveBeenCalledTimes(1); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(1); + + act(() => store.setState({...store.getState(), baz: 2})); + expect(componentRenderCount).toHaveBeenCalledTimes(1); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2); + + act(() => store.setState({...store.getState(), foo: {bar: 2}})); + expect(componentRenderCount).toHaveBeenCalledTimes(1); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2); + + act(() => store.setState({...store.getState(), foo: {bar: 3}})); + expect(componentRenderCount).toHaveBeenCalledTimes(2); + + act(() => store.setState({...store.getState()})); + expect(componentRenderCount).toHaveBeenCalledTimes(2); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2); + + act(() => store.setState({...store.getState(), foo: {}})); + expect(componentRenderCount).toHaveBeenCalledTimes(3); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2); + + act(() => store.setState({ foo: { bar: 3 }, baz: 10 })); + expect(componentRenderCount).toHaveBeenCalledTimes(4); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3); + + done(); + }) + }); +}); + +describe('store memoization with dispatch', () => { + it('Should not update components if setState is called with the same previous state', (done) => { + const store = createStore('reduceredStore', 0, (state, payload) => payload); + const componentRenderCount = jest.fn(); + const anotherComponentRenderCount = jest.fn(); + + const Component = (props) => { + const [state, dispatch] = useStore(store); + + componentRenderCount(); + + return ( + + ); + } + + const AnotherComponent = (props) => { + const [state] = useStore(store, ); + + anotherComponentRenderCount(); + + return ( +
+ {state} +
+ ); + } + + const renderedComponent = mount(); + const renderedAnotherComponent = mount(); + + requestAnimationFrame(() => { + expect(componentRenderCount).toHaveBeenCalledTimes(1); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(1); + + renderedComponent.find('button').simulate('click'); + expect(componentRenderCount).toHaveBeenCalledTimes(2); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2); + + act(() => store.dispatch(3)); + expect(componentRenderCount).toHaveBeenCalledTimes(3); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3); + + act(() => store.dispatch(3)); + expect(componentRenderCount).toHaveBeenCalledTimes(3); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3); + + act(() => store.dispatch(3)); + expect(componentRenderCount).toHaveBeenCalledTimes(3); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3); + + act(() => store.dispatch(1)); + expect(componentRenderCount).toHaveBeenCalledTimes(4); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(4); + + act(() => store.dispatch(1)); + expect(componentRenderCount).toHaveBeenCalledTimes(4); + expect(anotherComponentRenderCount).toHaveBeenCalledTimes(4); + + expect(renderedComponent.text()).toBe('This button has been clicked 1 times') + expect(renderedAnotherComponent.text()).toBe('1') + + done(); + }) + }); +}); From 26e07a0363451cd3680e299dd3581bdfc46351a5 Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sat, 8 Feb 2020 22:28:03 -0300 Subject: [PATCH 08/19] improving jsdoc --- example/basic.js | 2 +- src/index.js | 24 ++++-------------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/example/basic.js b/example/basic.js index dafc5e0..23ab226 100644 --- a/example/basic.js +++ b/example/basic.js @@ -8,7 +8,7 @@ const defaultStyles = { const mapStateToProps = (state) => state.number; // Creating a nameless store, do that if you do not wish to have multiple stores in your app -createStore('clickCounter', { number: 0 }, (state, number) => { +const store = createStore('clickCounter', { number: 0 }, (state, number) => { return { number } }); diff --git a/src/index.js b/src/index.js index da2ff7b..425c539 100644 --- a/src/index.js +++ b/src/index.js @@ -18,14 +18,9 @@ class StoreInterface { /** * Subscribe to store changes - * @callback callback - The function to be invoked everytime the store is updated + * @param {(state:any, action:any) => void} callback - The function to be invoked everytime the store is updated * @return {Function} - Call the function returned by the method to cancel the subscription */ - - /** - * - * @param {callback} state, action - */ subscribe(callback) { if (!callback || typeof callback !== 'function') { throw `store.subscribe callback argument must be a function. got '${typeof callback}' instead.`; @@ -61,14 +56,9 @@ function getStoreByIdentifier(identifier) { * Creates a new store * @param {String} name - The store namespace. * @param {*} state [{}] - The store initial state. It can be of any type. - * @callback reducer [null] + * @param {(state:any, action:any) => any} reducer [null] - The reducer handler. Optional * @returns {StoreInterface} The store instance. */ - - /** - * - * @param {reducer} prevState, action - The reducer handler. Optional. - */ export function createStore(name, state = {}, reducer=defaultReducer) { if (typeof name !== 'string') { throw 'Store name must be a string'; @@ -126,10 +116,9 @@ export function createStore(name, state = {}, reducer=defaultReducer) { /** * Returns a store instance based on its name - * @callback {String} name - The name of the wanted store + * @name {String} name - The name of the wanted store * @returns {StoreInterface} the store instance */ - export function getStoreByName(name) { try { return stores[name].public; @@ -141,14 +130,9 @@ export function getStoreByName(name) { /** * Returns a [ state, setState ] pair for the selected store. Can only be called within React Components * @param {String|StoreInterface} identifier - The identifier for the wanted store - * @callback memoFn [state => state] - A memoization function to optimize component rerender. Receive the store state and return a subset of it. The component will only rerender when the subset changes. + * @param {(state:any) => any} memoFn [state => state] - A memoization function to optimize component rerender. Receive the store state and return a subset of it. The component will only rerender when the subset changes. * @returns {Array} the [state, setState] pair. */ - -/** - * - * @param {memoFn} state - */ export function useStore(identifier, memoFn=defaultMemoFn) { const store = getStoreByIdentifier(identifier); if (!store) { From be6a84ec85dee3964b4ea5585ce15c1515f8a74d Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sat, 8 Feb 2020 23:38:37 -0300 Subject: [PATCH 09/19] using 'data' instead of the misleading 'action' on reducers --- src/index.js | 28 ++++++++++++++++++---------- types/index.d.ts | 24 ++++++++++++------------ 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/index.js b/src/index.js index 425c539..b6139ab 100644 --- a/src/index.js +++ b/src/index.js @@ -18,7 +18,7 @@ class StoreInterface { /** * Subscribe to store changes - * @param {(state:any, action:any) => void} callback - The function to be invoked everytime the store is updated + * @param {(state:any, data:any) => void} callback - The function to be invoked everytime the store is updated * @return {Function} - Call the function returned by the method to cancel the subscription */ subscribe(callback) { @@ -35,11 +35,18 @@ class StoreInterface { } } - setState() { + /** + * Set the store state + * @param {any} data - The new state value. + */ + setState(data) { console.warn(`[React Hookstore] Store ${this.name} uses a reducer to handle its state updates. use dispatch instead of setState`) } - - dispatch() { + /** + * Dispatch data to the store reducer + * @param {any} data - The data payload the reducer receives + */ + dispatch(data) { console.warn(`[React Hookstore] Store ${this.name} does not use a reducer to handle state updates. use setState instead of dispatch`) } } @@ -56,7 +63,7 @@ function getStoreByIdentifier(identifier) { * Creates a new store * @param {String} name - The store namespace. * @param {*} state [{}] - The store initial state. It can be of any type. - * @param {(state:any, action:any) => any} reducer [null] - The reducer handler. Optional + * @param {(state:any, data:any) => any} reducer [null] - The reducer handler. Optional * @returns {StoreInterface} The store instance. */ export function createStore(name, state = {}, reducer=defaultReducer) { @@ -66,15 +73,16 @@ export function createStore(name, state = {}, reducer=defaultReducer) { if (stores[name]) { throw `Store with name ${name} already exists`; } + const store = { state, reducer, - setState(action, callback) { + setState(data, callback) { const isPrimitiveStateWithoutReducerAndIsPreviousState = this.reducer === defaultReducer - && action === this.state - && typeof action !== 'object'; + && data === this.state + && typeof data !== 'object'; if (isPrimitiveStateWithoutReducerAndIsPreviousState) { if (typeof callback === 'function') callback(this.state) @@ -82,7 +90,7 @@ export function createStore(name, state = {}, reducer=defaultReducer) { } const currentState = this.state; - const newState = this.reducer(this.state, action); + const newState = this.reducer(this.state, data); this.state = newState; this.updatersPerMemoFunction.forEach((updaters, memoFn) => { @@ -97,7 +105,7 @@ export function createStore(name, state = {}, reducer=defaultReducer) { }); if (subscriptions[name].length) { - subscriptions[name].forEach(c => c(this.state, action)); + subscriptions[name].forEach(c => c(this.state, data)); } if (typeof callback === 'function') callback(this.state) diff --git a/types/index.d.ts b/types/index.d.ts index cce8c83..8f63115 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,22 +1,22 @@ declare module 'react-hookstore' { type StateCallback = (state: TState) => void; - type ReducerType = (state: TState, payload: TPayload) => TState; + type ReducerType = (state: TState, data: TData) => TState; type SetStateType = (state: TState, callback?: StateCallback) => void; - type DispatchType = (payload: TPayload, callback?: StateCallback) => void; + type DispatchType = (data: TData, callback?: StateCallback) => void; type StoreStateHookType = [TState, SetStateType]; - type StoreDispatchHookType = [TState, DispatchType]; + type StoreDispatchHookType = [TState, DispatchType]; const defaultReducer: ReducerType; - export interface StoreSpec { + export interface StoreSpec { state: TState; - reducer: ReducerType; - setState: SetStateType | DispatchType; + reducer: ReducerType; + setState: SetStateType | DispatchType; setters: StateCallback[] } @@ -26,27 +26,27 @@ declare module 'react-hookstore' { setState(state: TState, callback?: StateCallback): void; } - export interface ReducerStoreInterface { + export interface ReducerStoreInterface { readonly name: string; getState(): TState; - dispatch(payload: TPayload, callback?: StateCallback): void; + dispatch(data: TData, callback?: StateCallback): void; } - export function createStore(name: string, state: TState, reducer: ReducerType): ReducerStoreInterface; + export function createStore(name: string, state: TState, reducer: ReducerType): ReducerStoreInterface; export function createStore(name: string, state: TState): StateStoreInterface; export function createStore(name: string, state: TState, reducer: ReducerType): ReducerStoreInterface; - export function getStoreByName(name: string): StateStoreInterface | ReducerStoreInterface; + export function getStoreByName(name: string): StateStoreInterface | ReducerStoreInterface; export function getStoreByName(name: string): StateStoreInterface | ReducerStoreInterface; export function useStore(identifier: string): StoreStateHookType | StoreDispatchHookType; - export function useStore(identifier: string): StoreDispatchHookType; + export function useStore(identifier: string): StoreDispatchHookType; export function useStore(store: StateStoreInterface): StoreStateHookType; - export function useStore(store: ReducerStoreInterface): StoreDispatchHookType; + export function useStore(store: ReducerStoreInterface): StoreDispatchHookType; } From a5ef199a7ee1e8af6f84e2f295edda02f2084079 Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sat, 8 Feb 2020 23:56:21 -0300 Subject: [PATCH 10/19] resetting basic example --- example/basic.js | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/example/basic.js b/example/basic.js index 23ab226..58fc386 100644 --- a/example/basic.js +++ b/example/basic.js @@ -5,31 +5,18 @@ const defaultStyles = { padding: 10, backgroundColor: 'navy', marginTop: 10, color: 'white' } -const mapStateToProps = (state) => state.number; - // Creating a nameless store, do that if you do not wish to have multiple stores in your app -const store = createStore('clickCounter', { number: 0 }, (state, number) => { - return { number } -}); - -let delay = 0; -function setDelay() { - delay += 0.5; - if (delay > 1) { - delay = 0; - } - return delay; -} +createStore('clickCounter', 0); export function StatefulHello() { // just use the useStore method to grab the state and the setState methods - const [ state, setState ] = useStore('clickCounter', mapStateToProps); + const [ state, setState ] = useStore('clickCounter'); return (

Hello, component!

-

The button inside this component was clicked {state.number} times

- +

The button inside this component was clicked {state} times

+
); } @@ -40,7 +27,7 @@ export function AnotherComponent() { return (

Hello, this is a second component, with no relation to the one on the top

-

But it is still aware of how many times the button was clicked: {value.number}

+

But it is still aware of how many times the button was clicked: {value}

) } From 5a662cbc8aca511de8dc94c9bf91e7ee0ff65121 Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sun, 9 Feb 2020 00:17:20 -0300 Subject: [PATCH 11/19] improving documentation --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 781c1c2..0729e21 100644 --- a/README.md +++ b/README.md @@ -172,17 +172,17 @@ export { TodoList, AddTodo }; Check out the [Codesandbox demo!](https://codesandbox.io/s/r58pqonkop) ## Methods API -### `createStore(name:String, state?:*, reducer?:Function):StoreInterface` +### `createStore(name:String, state?:any, reducer?:(state:any, data:any) -> any) -> StoreInterface` Creates a store to be used across the entire application. Returns a StoreInterface object. ### Arguments #### `name:String` The namespace for your store, it can be used to identify the store across the application. -#### `state:* = {}` +#### `state:any = {}` The store's initial state. it can be any data type. defaults to an empty object. Optional -#### `reducer:Function` -You can specify a reducer function to take care of state changes. the reducer functions receives two arguments, the previous state and the action that triggered the state update. the function must return a new state, if not, the new state will be `null`. Optional +#### `reducer:(state:any, data:any) -> any` +You can specify a reducer function to take care of state changes. the reducer functions receives two arguments, the previous state and the data dispatched. the function must return a new state, if not, the new state will be `null`. Optional -### `getStoreByName(name:String):StoreInterface` +### `getStoreByName(name:String) -> StoreInterface` Finds a store by its name and returns its instance. ### Arguments #### `name:String` @@ -194,19 +194,19 @@ The store instance that is returned by the createStore and getStoreByName method ### Interface #### `name:String` The name of the store; -#### `getState:Function():*` +#### `getState:() -> state` A method that returns the store's current state -#### `setState:Function(state:*, callback?:Function)` +#### `setState:(state:any, callback?:(state:any) -> void)` Sets the state of the store. works if the store does not use a reducer state handler. Otherwise, use `dispatch`. callback is optional and will be invoked once the state is updated, receiving the updated state as argument. -#### `dispatch:Function(action:*, callback?:Function)` -Dispatches whatever is passed into this function to the store. works if the store uses a reducer state handler. Otherwise, use `setState`. callback is optional and will be invoked once the state is updated, receiving the updated state as argument. -#### `subscribe:Function(callback:Function):unsubscribe:Function` -The callback function will be invoked everytime the store state changes. If the store is reducer-based, the callback function will be called with `action` as the first argument and `state` as the second. otherwise, it'll be called with `state` as the only argument. +#### `dispatch(data:any, callback?(state:any) -> void)` +Dispatches data to update the state. works if the store uses a reducer state handler. Otherwise, use `setState`. callback is optional and will be invoked once the state is updated, receiving the updated state as argument. +#### `subscribe(callback(state) | callback(state, data)) -> unsubscribe:() -> void` +The callback function will be invoked everytime the store state changes. If the store is reducer-based, the callback function will be called with the state and the dispatched data as arguments. otherwise, it'll be called with state as the only argument. the subscribe method returns a function that can be called in order to cancel the subscription for the callback function. ## React API -### `useStore(identifier:String|StoreInterface):Array[state, setState|dispatch]` +### `useStore(identifier:String|StoreInterface) -> [state, setState|dispatch]` A function that returns a pair with the current state and a function to trigger state updates for the specified store. ### Arguments #### Identifier:String|StoreInterface @@ -221,11 +221,11 @@ createStore({state: 0}); createStore({ name: 'store', state: 0, - reducer(state, action) { - return state + action; + reducer(state, data) { + return state + data; } }) // v1.1 createStore('myStore', 0); -createStore('store', 0, (state, value) => state + action); +createStore('store', 0, (state, data) => state + data); ``` From ca2a39fee38eb78e00893650503fa1805d462891 Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sun, 9 Feb 2020 00:25:03 -0300 Subject: [PATCH 12/19] improving docs --- README.md | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0729e21..f66be1e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # React Hook Store [![npm version](https://badge.fury.io/js/react-hookstore.svg)](https://badge.fury.io/js/react-hookstore) [![Build Status](https://travis-ci.org/jhonnymichel/react-hookstore.svg?branch=master)](https://travis-ci.org/jhonnymichel/react-hookstore) [![Coverage Status](https://coveralls.io/repos/github/jhonnymichel/react-hookstore/badge.svg?branch=master)](https://coveralls.io/github/jhonnymichel/react-hookstore?branch=master) -A very simple and small (1k gzipped!) state management lib for React that uses the bleeding edge React's `useState` hook. -Which basically means no magic behind the curtains, only pure react APIs being used to share state across components. +A very simple and small (less than 2k gzipped!) state management lib for React using hooks. Try it on [Codesandbox!](https://codesandbox.io/s/r58pqonkop) @@ -11,7 +10,7 @@ Try it on [Codesandbox!](https://codesandbox.io/s/r58pqonkop) - Usage - [Basic](#usage_basic) - [Referencing stores](#usage_namespace) - - [Reducer powered stores](#usage_reducer) + - [Using reducers to update state](#usage_reducer) - [More examples](https://codesandbox.io/s/r58pqonkop) - API - [createStore](#api_createStore) @@ -30,9 +29,7 @@ You can install the lib through NPM or grab the files in the `dist` folder of th ## Usage ### Basic -This is the most basic implementation of the library. create a store with its initial state. -Later, call `useStore` inside components to retrieve its state and setState method. -The value passed as the first argument to the setState method will be the new state. no reducer required (but you can use a reducer, see the advanced example down below). +This is the most basic implementation of the library. ```javascript import React from 'react'; @@ -64,9 +61,13 @@ function AnotherComponent() { ) } ``` +Steps to reproduce: + +- Create a store with its initial state. +- Later, call `useStore` inside components to retrieve its state and setState method, that we called timesClicked and setClicks. +- The value passed as the first argument to the setClicks method will be the new state. ### Referencing stores -It is possible to create multiple stores in an app. Stores can be referenced by using their instance that is returned by the createStore method, as well as using their name. ```javascript @@ -95,9 +96,9 @@ function StatefullHello() { ); } ``` -Both methods can be used and mixed according to the needs, but we recomend using the instance identifiers. +Both methods can be used and mixed according to the needs, but it is recomended to use the instance identifiers. -### Reducer powered stores +### Using reducers to update state We can delegate the state management to reducers (just like redux!) if we want. ```javascript import React from 'react'; @@ -168,6 +169,14 @@ function TodoList() { export { TodoList, AddTodo }; ``` + +Steps to reproduce: + +- Create a store with an aditional third parameter: a reducer function. +- Later, call `useStore` inside components to retrieve its state and dispatch method. +- call dispatch and provide data as the first argument. Although data can be anything, we are using the pattern of { type, payload }, made popular by redux. + + ### More examples Check out the [Codesandbox demo!](https://codesandbox.io/s/r58pqonkop) From 1b0b7dfbcb4e89b5dd7ae7717d94feb6e353937b Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sun, 9 Feb 2020 00:28:55 -0300 Subject: [PATCH 13/19] displaying method signature under its title --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f66be1e..44eff96 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ You can install the lib through NPM or grab the files in the `dist` folder of th ## Usage ### Basic -This is the most basic implementation of the library. +This is the most basic implementation of the library: ```javascript import React from 'react'; @@ -38,7 +38,6 @@ import { createStore, useStore } from 'react-hookstore'; createStore('clickStore', 0); function StatefullHello() { - // just use the useStore method to grab the state and the setState methods const [ timesClicked, setClicks ] = useStore('clickStore'); return ( @@ -51,7 +50,6 @@ function StatefullHello() { } function AnotherComponent() { - // you can name the state whatever you want const [ timesClicked ] = useStore('clickStore'); return (
@@ -99,7 +97,7 @@ function StatefullHello() { Both methods can be used and mixed according to the needs, but it is recomended to use the instance identifiers. ### Using reducers to update state -We can delegate the state management to reducers (just like redux!) if we want. +We can delegate the state management to reducers (just like redux!) if we want: ```javascript import React from 'react'; import { createStore, useStore } from 'react-hookstore'; @@ -174,14 +172,15 @@ Steps to reproduce: - Create a store with an aditional third parameter: a reducer function. - Later, call `useStore` inside components to retrieve its state and dispatch method. -- call dispatch and provide data as the first argument. Although data can be anything, we are using the pattern of { type, payload }, made popular by redux. +- call dispatch and provide data as the first argument. Although data can be anything, we are using the pattern of `{ type, payload }`, made popular by redux. ### More examples Check out the [Codesandbox demo!](https://codesandbox.io/s/r58pqonkop) ## Methods API -### `createStore(name:String, state?:any, reducer?:(state:any, data:any) -> any) -> StoreInterface` +### `createStore` +`createStore(name:String, state?:any, reducer?:(state:any, data:any) -> any) -> StoreInterface` Creates a store to be used across the entire application. Returns a StoreInterface object. ### Arguments #### `name:String` From 29a6b21ce4135e1b924a2991af3669cd4add9360 Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sun, 9 Feb 2020 00:37:19 -0300 Subject: [PATCH 14/19] improving docs --- README.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 44eff96..223cc71 100644 --- a/README.md +++ b/README.md @@ -179,42 +179,41 @@ Steps to reproduce: Check out the [Codesandbox demo!](https://codesandbox.io/s/r58pqonkop) ## Methods API -### `createStore` -`createStore(name:String, state?:any, reducer?:(state:any, data:any) -> any) -> StoreInterface` +### `createStore(name: String, state?: any, reducer?: (state:any, data:any) -> any) -> StoreInterface` Creates a store to be used across the entire application. Returns a StoreInterface object. ### Arguments -#### `name:String` +#### `name: String` The namespace for your store, it can be used to identify the store across the application. -#### `state:any = {}` +#### `state: any = {}` The store's initial state. it can be any data type. defaults to an empty object. Optional -#### `reducer:(state:any, data:any) -> any` +#### `reducer: (state:any, data:any) -> any` You can specify a reducer function to take care of state changes. the reducer functions receives two arguments, the previous state and the data dispatched. the function must return a new state, if not, the new state will be `null`. Optional -### `getStoreByName(name:String) -> StoreInterface` +### `getStoreByName(name: String) -> StoreInterface` Finds a store by its name and returns its instance. ### Arguments -#### `name:String` +#### `name: String` The name of the store. ## Objects API ### `StoreInterface` The store instance that is returned by the createStore and getStoreByName methods. ### Interface -#### `name:String` +#### `name: String` The name of the store; -#### `getState:() -> state` +#### `getState: () -> state` A method that returns the store's current state -#### `setState:(state:any, callback?:(state:any) -> void)` +#### `setState: (state: any, callback?: (state:any) -> void)` Sets the state of the store. works if the store does not use a reducer state handler. Otherwise, use `dispatch`. callback is optional and will be invoked once the state is updated, receiving the updated state as argument. -#### `dispatch(data:any, callback?(state:any) -> void)` +#### `dispatch(data: any, callback?: (state: any) -> void)` Dispatches data to update the state. works if the store uses a reducer state handler. Otherwise, use `setState`. callback is optional and will be invoked once the state is updated, receiving the updated state as argument. -#### `subscribe(callback(state) | callback(state, data)) -> unsubscribe:() -> void` +#### `subscribe(callback: (state) -> void | callback: (state, data) -> void) -> unsubscribe: () -> void` The callback function will be invoked everytime the store state changes. If the store is reducer-based, the callback function will be called with the state and the dispatched data as arguments. otherwise, it'll be called with state as the only argument. the subscribe method returns a function that can be called in order to cancel the subscription for the callback function. ## React API -### `useStore(identifier:String|StoreInterface) -> [state, setState|dispatch]` +### `useStore(identifier: String|StoreInterface) -> [state, setState|dispatch]` A function that returns a pair with the current state and a function to trigger state updates for the specified store. ### Arguments #### Identifier:String|StoreInterface From 70c3b28fa546d54c7f2aa6e164913bffbf141706 Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sun, 9 Feb 2020 00:41:20 -0300 Subject: [PATCH 15/19] improving docs --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 223cc71..76860ad 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ Steps to reproduce: Check out the [Codesandbox demo!](https://codesandbox.io/s/r58pqonkop) ## Methods API -### `createStore(name: String, state?: any, reducer?: (state:any, data:any) -> any) -> StoreInterface` +### `createStore(name, state?, reducer?) -> StoreInterface` Creates a store to be used across the entire application. Returns a StoreInterface object. ### Arguments #### `name: String` @@ -207,16 +207,16 @@ A method that returns the store's current state Sets the state of the store. works if the store does not use a reducer state handler. Otherwise, use `dispatch`. callback is optional and will be invoked once the state is updated, receiving the updated state as argument. #### `dispatch(data: any, callback?: (state: any) -> void)` Dispatches data to update the state. works if the store uses a reducer state handler. Otherwise, use `setState`. callback is optional and will be invoked once the state is updated, receiving the updated state as argument. -#### `subscribe(callback: (state) -> void | callback: (state, data) -> void) -> unsubscribe: () -> void` +#### `subscribe(callback: (state: any, data?: any) -> void) -> unsubscribe: () -> void` The callback function will be invoked everytime the store state changes. If the store is reducer-based, the callback function will be called with the state and the dispatched data as arguments. otherwise, it'll be called with state as the only argument. the subscribe method returns a function that can be called in order to cancel the subscription for the callback function. ## React API -### `useStore(identifier: String|StoreInterface) -> [state, setState|dispatch]` +### `useStore(identifier) -> [state, setState|dispatch]` A function that returns a pair with the current state and a function to trigger state updates for the specified store. ### Arguments -#### Identifier:String|StoreInterface +#### Identifier: String|StoreInterface The store identifier. It can be either its string name or its StoreInterface instance returned by a createStore or getStoreByName method. # Migrating from v1.0 to v1.1 From 52e201977804481bd45eb018378730609ef58503 Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sun, 9 Feb 2020 00:45:13 -0300 Subject: [PATCH 16/19] improving docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 76860ad..db0f621 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ The store's initial state. it can be any data type. defaults to an empty object. #### `reducer: (state:any, data:any) -> any` You can specify a reducer function to take care of state changes. the reducer functions receives two arguments, the previous state and the data dispatched. the function must return a new state, if not, the new state will be `null`. Optional -### `getStoreByName(name: String) -> StoreInterface` +### `getStoreByName(name) -> StoreInterface` Finds a store by its name and returns its instance. ### Arguments #### `name: String` From 639fa15ecc7fa6df11e8be164ae96890014fe49e Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sun, 9 Feb 2020 00:47:06 -0300 Subject: [PATCH 17/19] improving docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db0f621..8c10590 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ The store instance that is returned by the createStore and getStoreByName method The name of the store; #### `getState: () -> state` A method that returns the store's current state -#### `setState: (state: any, callback?: (state:any) -> void)` +#### `setState: (state: any, callback?: (state: any) -> void)` Sets the state of the store. works if the store does not use a reducer state handler. Otherwise, use `dispatch`. callback is optional and will be invoked once the state is updated, receiving the updated state as argument. #### `dispatch(data: any, callback?: (state: any) -> void)` Dispatches data to update the state. works if the store uses a reducer state handler. Otherwise, use `setState`. callback is optional and will be invoked once the state is updated, receiving the updated state as argument. From 734e407220dceb24b620f479689da69f7eaef386 Mon Sep 17 00:00:00 2001 From: Jhonny Michel Date: Sun, 9 Feb 2020 11:02:20 -0300 Subject: [PATCH 18/19] revamping docs --- README.md | 8 +- {example => dev-server}/basic.js | 0 {example => dev-server}/index.html | 0 {example => dev-server}/index.js | 0 {example => dev-server}/reducers.js | 0 {example => dev-server}/subscribe.js | 0 dist/react-hookstore.js | 115 ++++++++++++++++++++------- dist/react-hookstore.js.map | 2 +- dist/react-hookstore.min.js | 2 +- docs/demo.js | 32 -------- docs/demo.js.map | 1 - webpack.dev.config.js | 4 +- webpack.prod.config.js | 12 +-- 13 files changed, 99 insertions(+), 77 deletions(-) rename {example => dev-server}/basic.js (100%) rename {example => dev-server}/index.html (100%) rename {example => dev-server}/index.js (100%) rename {example => dev-server}/reducers.js (100%) rename {example => dev-server}/subscribe.js (100%) delete mode 100644 docs/demo.js delete mode 100644 docs/demo.js.map diff --git a/README.md b/README.md index 8c10590..27ec26d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Try it on [Codesandbox!](https://codesandbox.io/s/r58pqonkop) - [Basic](#usage_basic) - [Referencing stores](#usage_namespace) - [Using reducers to update state](#usage_reducer) + - [Optimizing performance](#optimizing_performance) - [More examples](https://codesandbox.io/s/r58pqonkop) - API - [createStore](#api_createStore) @@ -174,6 +175,7 @@ Steps to reproduce: - Later, call `useStore` inside components to retrieve its state and dispatch method. - call dispatch and provide data as the first argument. Although data can be anything, we are using the pattern of `{ type, payload }`, made popular by redux. +### Optimizing performance ### More examples Check out the [Codesandbox demo!](https://codesandbox.io/s/r58pqonkop) @@ -213,11 +215,13 @@ The callback function will be invoked everytime the store state changes. If the the subscribe method returns a function that can be called in order to cancel the subscription for the callback function. ## React API -### `useStore(identifier) -> [state, setState|dispatch]` +### `useStore(identifier, memoFn?) -> [state, setState|dispatch]` A function that returns a pair with the current state and a function to trigger state updates for the specified store. ### Arguments -#### Identifier: String|StoreInterface +#### `Identifier: String|StoreInterface` The store identifier. It can be either its string name or its StoreInterface instance returned by a createStore or getStoreByName method. +#### `memoFn: (state) -> any` +A function to optimize performance. return the subset of the state the component is dependent on. The component will only be updated when the subset changes. Optional. # Migrating from v1.0 to v1.1 - createStore now receives 3 arguments instead of an object with 3 properties. diff --git a/example/basic.js b/dev-server/basic.js similarity index 100% rename from example/basic.js rename to dev-server/basic.js diff --git a/example/index.html b/dev-server/index.html similarity index 100% rename from example/index.html rename to dev-server/index.html diff --git a/example/index.js b/dev-server/index.js similarity index 100% rename from example/index.js rename to dev-server/index.js diff --git a/example/reducers.js b/dev-server/reducers.js similarity index 100% rename from example/reducers.js rename to dev-server/reducers.js diff --git a/example/subscribe.js b/dev-server/subscribe.js similarity index 100% rename from example/subscribe.js rename to dev-server/subscribe.js diff --git a/dist/react-hookstore.js b/dist/react-hookstore.js index 1726047..a831fe7 100644 --- a/dist/react-hookstore.js +++ b/dist/react-hookstore.js @@ -136,6 +136,10 @@ var subscriptions = {}; var defaultReducer = function defaultReducer(state, payload) { return payload; }; + +var defaultMemoFn = function defaultMemoFn(state) { + return state; +}; /** The public interface of a store */ @@ -156,15 +160,10 @@ function () { } /** * Subscribe to store changes - * @callback callback - The function to be invoked everytime the store is updated + * @param {(state:any, data:any) => void} callback - The function to be invoked everytime the store is updated * @return {Function} - Call the function returned by the method to cancel the subscription */ - /** - * - * @param {callback} state, action - */ - _createClass(StoreInterface, [{ key: "subscribe", @@ -189,14 +188,24 @@ function () { }); }; } + /** + * Set the store state + * @param {any} data - The new state value. + */ + }, { key: "setState", - value: function setState() { + value: function setState(data) { console.warn("[React Hookstore] Store ".concat(this.name, " uses a reducer to handle its state updates. use dispatch instead of setState")); } + /** + * Dispatch data to the store reducer + * @param {any} data - The data payload the reducer receives + */ + }, { key: "dispatch", - value: function dispatch() { + value: function dispatch(data) { console.warn("[React Hookstore] Store ".concat(this.name, " does not use a reducer to handle state updates. use setState instead of dispatch")); } }]); @@ -217,15 +226,10 @@ function getStoreByIdentifier(identifier) { * Creates a new store * @param {String} name - The store namespace. * @param {*} state [{}] - The store initial state. It can be of any type. - * @callback reducer [null] + * @param {(state:any, data:any) => any} reducer [null] - The reducer handler. Optional * @returns {StoreInterface} The store instance. */ -/** - * - * @param {reducer} prevState, action - The reducer handler. Optional. - */ - function createStore(name) { var state = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; @@ -242,33 +246,72 @@ function createStore(name) { var store = { state: state, reducer: reducer, - setState: function setState(action, callback) { + setState: function setState(data, callback) { var _this2 = this; - this.state = this.reducer(this.state, action); - this.setters.forEach(function (setter) { - return setter(_this2.state); + var isPrimitiveStateWithoutReducerAndIsPreviousState = this.reducer === defaultReducer && data === this.state && _typeof(data) !== 'object'; + + if (isPrimitiveStateWithoutReducerAndIsPreviousState) { + if (typeof callback === 'function') callback(this.state); + return; + } + + var currentState = this.state; + var newState = this.reducer(this.state, data); + this.state = newState; + this.updatersPerMemoFunction.forEach(function (updaters, memoFn) { + var prevResult = memoFn(currentState); + var newResult = memoFn(newState); + + if (prevResult === newResult) { + return; + } + + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = updaters[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var updateComponent = _step.value; + updateComponent(_this2.state); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return != null) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } }); if (subscriptions[name].length) { subscriptions[name].forEach(function (c) { - return c(_this2.state, action); + return c(_this2.state, data); }); } if (typeof callback === 'function') callback(this.state); }, - setters: [] + updatersPerMemoFunction: new Map() }; store.setState = store.setState.bind(store); + store.updatersPerMemoFunction.set(defaultMemoFn, new Set()); + stores = Object.assign({}, stores, _defineProperty({}, name, store)); subscriptions[name] = []; store.public = new StoreInterface(name, store, reducer !== defaultReducer); - stores = Object.assign({}, stores, _defineProperty({}, name, store)); return store.public; } /** * Returns a store instance based on its name - * @callback {String} name - The name of the wanted store + * @name {String} name - The name of the wanted store * @returns {StoreInterface} the store instance */ @@ -282,26 +325,44 @@ function getStoreByName(name) { /** * Returns a [ state, setState ] pair for the selected store. Can only be called within React Components * @param {String|StoreInterface} identifier - The identifier for the wanted store + * @param {(state:any) => any} memoFn [state => state] - A memoization function to optimize component rerender. Receive the store state and return a subset of it. The component will only rerender when the subset changes. * @returns {Array} the [state, setState] pair. */ function useStore(identifier) { + var memoFn = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultMemoFn; var store = getStoreByIdentifier(identifier); + if (!store) { + throw 'store does not exist'; + } + + if (typeof memoFn !== 'function') { + throw 'memoFn must be a function'; + } + var _useState = Object(react__WEBPACK_IMPORTED_MODULE_0__["useState"])(store.state), _useState2 = _slicedToArray(_useState, 2), state = _useState2[0], set = _useState2[1]; Object(react__WEBPACK_IMPORTED_MODULE_0__["useEffect"])(function () { - if (!store.setters.includes(set)) { - store.setters.push(set); + if (!store.updatersPerMemoFunction.has(memoFn)) { + store.updatersPerMemoFunction.set(memoFn, new Set()); + } + + var updatersPerMemoFunction = store.updatersPerMemoFunction.get(memoFn); + + if (!updatersPerMemoFunction.has(set)) { + updatersPerMemoFunction.add(set); } return function () { - store.setters = store.setters.filter(function (setter) { - return setter !== set; - }); + updatersPerMemoFunction.delete(set); + + if (!updatersPerMemoFunction.size) { + store.updatersPerMemoFunction.delete(memoFn); + } }; }, []); return [state, store.setState]; diff --git a/dist/react-hookstore.js.map b/dist/react-hookstore.js.map index 1e590a8..b1d70a0 100644 --- a/dist/react-hookstore.js.map +++ b/dist/react-hookstore.js.map @@ -1 +1 @@ -{"version":3,"sources":["webpack://hookStore/webpack/universalModuleDefinition","webpack://hookStore/webpack/bootstrap","webpack://hookStore/external {\"commonjs\":\"react\",\"commonjs2\":\"react\",\"amd\":\"React\",\"root\":\"React\"}","webpack://hookStore/./src/index.js"],"names":["stores","subscriptions","defaultReducer","state","payload","StoreInterface","name","store","useReducer","dispatch","setState","getState","subscribe","bind","callback","find","c","console","warn","push","filter","getStoreByIdentifier","identifier","createStore","reducer","action","setters","forEach","setter","length","public","Object","assign","getStoreByName","e","useStore","useState","set","useEffect","includes"],"mappings":"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","file":"react-hookstore.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"react\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"React\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"hookStore\"] = factory(require(\"react\"));\n\telse\n\t\troot[\"hookStore\"] = factory(root[\"React\"]);\n})(typeof self !== 'undefined' ? self : this, function(__WEBPACK_EXTERNAL_MODULE__0__) {\nreturn "," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 1);\n","module.exports = __WEBPACK_EXTERNAL_MODULE__0__;","import { useState, useEffect } from 'react';\n\nlet stores = {};\nlet subscriptions = {};\n\nconst defaultReducer = (state, payload) => payload;\n\n/** The public interface of a store */\nclass StoreInterface {\n constructor(name, store, useReducer) {\n this.name = name;\n useReducer ?\n this.dispatch = store.setState : this.setState = store.setState;\n this.getState = () => store.state;\n this.subscribe = this.subscribe.bind(this);\n }\n\n /**\n * Subscribe to store changes\n * @callback callback - The function to be invoked everytime the store is updated\n * @return {Function} - Call the function returned by the method to cancel the subscription\n */\n\n /**\n *\n * @param {callback} state, action\n */\n subscribe(callback) {\n if (!callback || typeof callback !== 'function') {\n throw `store.subscribe callback argument must be a function. got '${typeof callback}' instead.`;\n }\n if (subscriptions[this.name].find(c => c === callback)) {\n console.warn('This callback is already subscribed to this store. skipping subscription');\n return;\n }\n subscriptions[this.name].push(callback);\n return () => {\n subscriptions[this.name] = subscriptions[this.name].filter(c => c !== callback);\n }\n }\n\n setState() {\n console.warn(`[React Hookstore] Store ${this.name} uses a reducer to handle its state updates. use dispatch instead of setState`)\n }\n\n dispatch() {\n console.warn(`[React Hookstore] Store ${this.name} does not use a reducer to handle state updates. use setState instead of dispatch`)\n }\n}\n\nfunction getStoreByIdentifier(identifier) {\n const name = identifier instanceof StoreInterface ? identifier.name : identifier;\n if (!stores[name]) {\n throw `Store with name ${name} does not exist`;\n }\n return stores[name];\n}\n\n/**\n * Creates a new store\n * @param {String} name - The store namespace.\n * @param {*} state [{}] - The store initial state. It can be of any type.\n * @callback reducer [null]\n * @returns {StoreInterface} The store instance.\n */\n\n /**\n *\n * @param {reducer} prevState, action - The reducer handler. Optional.\n */\nexport function createStore(name, state = {}, reducer=defaultReducer) {\n if (typeof name !== 'string') {\n throw 'Store name must be a string';\n }\n if (stores[name]) {\n throw `Store with name ${name} already exists`;\n }\n\n const store = {\n state,\n reducer,\n setState(action, callback) {\n this.state = this.reducer(this.state, action);\n this.setters.forEach(setter => setter(this.state));\n if (subscriptions[name].length) {\n subscriptions[name].forEach(c => c(this.state, action));\n }\n if (typeof callback === 'function') callback(this.state)\n },\n setters: []\n };\n store.setState = store.setState.bind(store);\n subscriptions[name] = [];\n store.public = new StoreInterface(name, store, reducer !== defaultReducer);\n\n stores = Object.assign({}, stores, { [name]: store });\n return store.public;\n}\n\n/**\n * Returns a store instance based on its name\n * @callback {String} name - The name of the wanted store\n * @returns {StoreInterface} the store instance\n */\n\nexport function getStoreByName(name) {\n try {\n return stores[name].public;\n } catch(e) {\n throw `Store with name ${name} does not exist`;\n }\n}\n\n/**\n * Returns a [ state, setState ] pair for the selected store. Can only be called within React Components\n * @param {String|StoreInterface} identifier - The identifier for the wanted store\n * @returns {Array} the [state, setState] pair.\n */\nexport function useStore(identifier) {\n const store = getStoreByIdentifier(identifier);\n\n const [ state, set ] = useState(store.state);\n\n useEffect(() => {\n if (!store.setters.includes(set)) {\n store.setters.push(set);\n }\n\n return () => {\n store.setters = store.setters.filter(setter => setter !== set)\n }\n }, [])\n\n return [ state, store.setState ];\n}\n"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack://hookStore/webpack/universalModuleDefinition","webpack://hookStore/webpack/bootstrap","webpack://hookStore/external {\"commonjs\":\"react\",\"commonjs2\":\"react\",\"amd\":\"React\",\"root\":\"React\"}","webpack://hookStore/./src/index.js"],"names":["stores","subscriptions","defaultReducer","state","payload","defaultMemoFn","StoreInterface","name","store","useReducer","dispatch","setState","getState","subscribe","bind","callback","find","c","console","warn","push","filter","data","getStoreByIdentifier","identifier","createStore","reducer","isPrimitiveStateWithoutReducerAndIsPreviousState","currentState","newState","updatersPerMemoFunction","forEach","updaters","memoFn","prevResult","newResult","updateComponent","length","Map","set","Set","Object","assign","public","getStoreByName","e","useStore","useState","useEffect","has","get","add","delete","size"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;ACVA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,kDAA0C,gCAAgC;AAC1E;AACA;;AAEA;AACA;AACA;AACA,gEAAwD,kBAAkB;AAC1E;AACA,yDAAiD,cAAc;AAC/D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iDAAyC,iCAAiC;AAC1E,wHAAgH,mBAAmB,EAAE;AACrI;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;;AAGA;AACA;;;;;;;AClFA,gD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACAA;AAEA,IAAIA,MAAM,GAAG,EAAb;AACA,IAAIC,aAAa,GAAG,EAApB;;AAEA,IAAMC,cAAc,GAAG,SAAjBA,cAAiB,CAACC,KAAD,EAAQC,OAAR;AAAA,SAAoBA,OAApB;AAAA,CAAvB;;AACA,IAAMC,aAAa,GAAG,SAAhBA,aAAgB,CAACF,KAAD;AAAA,SAAWA,KAAX;AAAA,CAAtB;AAEA;;;IACMG,c;;;AACJ,0BAAYC,IAAZ,EAAkBC,KAAlB,EAAyBC,UAAzB,EAAqC;AAAA;;AACnC,SAAKF,IAAL,GAAYA,IAAZ;AACAE,cAAU,GACR,KAAKC,QAAL,GAAgBF,KAAK,CAACG,QADd,GACyB,KAAKA,QAAL,GAAgBH,KAAK,CAACG,QADzD;;AAEA,SAAKC,QAAL,GAAgB;AAAA,aAAMJ,KAAK,CAACL,KAAZ;AAAA,KAAhB;;AACA,SAAKU,SAAL,GAAiB,KAAKA,SAAL,CAAeC,IAAf,CAAoB,IAApB,CAAjB;AACD;AAED;;;;;;;;;8BAKUC,Q,EAAU;AAAA;;AAClB,UAAI,CAACA,QAAD,IAAa,OAAOA,QAAP,KAAoB,UAArC,EAAiD;AAC/C,2FAA2EA,QAA3E;AACD;;AACD,UAAId,aAAa,CAAC,KAAKM,IAAN,CAAb,CAAyBS,IAAzB,CAA8B,UAAAC,CAAC;AAAA,eAAIA,CAAC,KAAKF,QAAV;AAAA,OAA/B,CAAJ,EAAwD;AACtDG,eAAO,CAACC,IAAR,CAAa,0EAAb;AACA;AACD;;AACDlB,mBAAa,CAAC,KAAKM,IAAN,CAAb,CAAyBa,IAAzB,CAA8BL,QAA9B;AACA,aAAO,YAAM;AACXd,qBAAa,CAAC,KAAI,CAACM,IAAN,CAAb,GAA2BN,aAAa,CAAC,KAAI,CAACM,IAAN,CAAb,CAAyBc,MAAzB,CAAgC,UAAAJ,CAAC;AAAA,iBAAIA,CAAC,KAAKF,QAAV;AAAA,SAAjC,CAA3B;AACD,OAFD;AAGD;AAED;;;;;;;6BAISO,I,EAAM;AACbJ,aAAO,CAACC,IAAR,mCAAwC,KAAKZ,IAA7C;AACD;AACD;;;;;;;6BAISe,I,EAAM;AACbJ,aAAO,CAACC,IAAR,mCAAwC,KAAKZ,IAA7C;AACD;;;;;;AAGH,SAASgB,oBAAT,CAA8BC,UAA9B,EAA0C;AACxC,MAAMjB,IAAI,GAAGiB,UAAU,YAAYlB,cAAtB,GAAuCkB,UAAU,CAACjB,IAAlD,GAAyDiB,UAAtE;;AACA,MAAI,CAACxB,MAAM,CAACO,IAAD,CAAX,EAAmB;AACjB,oCAAyBA,IAAzB;AACD;;AACD,SAAOP,MAAM,CAACO,IAAD,CAAb;AACD;AAED;;;;;;;;;AAOO,SAASkB,WAAT,CAAqBlB,IAArB,EAA+D;AAAA,MAApCJ,KAAoC,uEAA5B,EAA4B;AAAA,MAAxBuB,OAAwB,uEAAhBxB,cAAgB;;AACpE,MAAI,OAAOK,IAAP,KAAgB,QAApB,EAA8B;AAC5B,UAAM,6BAAN;AACD;;AACD,MAAIP,MAAM,CAACO,IAAD,CAAV,EAAkB;AAChB,oCAAyBA,IAAzB;AACD;;AAGD,MAAMC,KAAK,GAAG;AACZL,SAAK,EAALA,KADY;AAEZuB,WAAO,EAAPA,OAFY;AAGZf,YAHY,oBAGHW,IAHG,EAGGP,QAHH,EAGa;AAAA;;AACvB,UAAMY,gDAAgD,GACpD,KAAKD,OAAL,KAAiBxB,cAAjB,IACKoB,IAAI,KAAK,KAAKnB,KADnB,IAEK,QAAOmB,IAAP,MAAgB,QAHvB;;AAKA,UAAIK,gDAAJ,EAAsD;AACpD,YAAI,OAAOZ,QAAP,KAAoB,UAAxB,EAAoCA,QAAQ,CAAC,KAAKZ,KAAN,CAAR;AACpC;AACD;;AAED,UAAMyB,YAAY,GAAG,KAAKzB,KAA1B;AACA,UAAM0B,QAAQ,GAAG,KAAKH,OAAL,CAAa,KAAKvB,KAAlB,EAAyBmB,IAAzB,CAAjB;AACA,WAAKnB,KAAL,GAAa0B,QAAb;AAEA,WAAKC,uBAAL,CAA6BC,OAA7B,CAAqC,UAACC,QAAD,EAAWC,MAAX,EAAsB;AACzD,YAAMC,UAAU,GAAGD,MAAM,CAACL,YAAD,CAAzB;AACA,YAAMO,SAAS,GAAGF,MAAM,CAACJ,QAAD,CAAxB;;AACA,YAAIK,UAAU,KAAKC,SAAnB,EAA8B;AAC5B;AACD;;AALwD;AAAA;AAAA;;AAAA;AAMzD,+BAA4BH,QAA5B,8HAAsC;AAAA,gBAA7BI,eAA6B;AACpCA,2BAAe,CAAC,MAAI,CAACjC,KAAN,CAAf;AACD;AARwD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS1D,OATD;;AAWA,UAAIF,aAAa,CAACM,IAAD,CAAb,CAAoB8B,MAAxB,EAAgC;AAC9BpC,qBAAa,CAACM,IAAD,CAAb,CAAoBwB,OAApB,CAA4B,UAAAd,CAAC;AAAA,iBAAIA,CAAC,CAAC,MAAI,CAACd,KAAN,EAAamB,IAAb,CAAL;AAAA,SAA7B;AACD;;AAED,UAAI,OAAOP,QAAP,KAAoB,UAAxB,EAAoCA,QAAQ,CAAC,KAAKZ,KAAN,CAAR;AACrC,KAlCW;AAmCZ2B,2BAAuB,EAAE,IAAIQ,GAAJ;AAnCb,GAAd;AAsCA9B,OAAK,CAACG,QAAN,GAAiBH,KAAK,CAACG,QAAN,CAAeG,IAAf,CAAoBN,KAApB,CAAjB;AACAA,OAAK,CAACsB,uBAAN,CAA8BS,GAA9B,CAAkClC,aAAlC,EAAiD,IAAImC,GAAJ,EAAjD;AACAxC,QAAM,GAAGyC,MAAM,CAACC,MAAP,CAAc,EAAd,EAAkB1C,MAAlB,sBAA6BO,IAA7B,EAAoCC,KAApC,EAAT;AACAP,eAAa,CAACM,IAAD,CAAb,GAAsB,EAAtB;AAEAC,OAAK,CAACmC,MAAN,GAAe,IAAIrC,cAAJ,CAAmBC,IAAnB,EAAyBC,KAAzB,EAAgCkB,OAAO,KAAKxB,cAA5C,CAAf;AACA,SAAOM,KAAK,CAACmC,MAAb;AACD;AAED;;;;;;AAKO,SAASC,cAAT,CAAwBrC,IAAxB,EAA8B;AACnC,MAAI;AACF,WAAOP,MAAM,CAACO,IAAD,CAAN,CAAaoC,MAApB;AACD,GAFD,CAEE,OAAME,CAAN,EAAS;AACT,oCAAyBtC,IAAzB;AACD;AACF;AAED;;;;;;;AAMO,SAASuC,QAAT,CAAkBtB,UAAlB,EAAoD;AAAA,MAAtBS,MAAsB,uEAAf5B,aAAe;AACzD,MAAMG,KAAK,GAAGe,oBAAoB,CAACC,UAAD,CAAlC;;AACA,MAAI,CAAChB,KAAL,EAAY;AACV,UAAM,sBAAN;AACD;;AACD,MAAI,OAAOyB,MAAP,KAAkB,UAAtB,EAAkC;AAChC,UAAM,2BAAN;AACD;;AAPwD,kBASlCc,sDAAQ,CAACvC,KAAK,CAACL,KAAP,CAT0B;AAAA;AAAA,MASjDA,KATiD;AAAA,MAS1CoC,GAT0C;;AAWzDS,yDAAS,CAAC,YAAM;AACd,QAAI,CAACxC,KAAK,CAACsB,uBAAN,CAA8BmB,GAA9B,CAAkChB,MAAlC,CAAL,EAAgD;AAC9CzB,WAAK,CAACsB,uBAAN,CAA8BS,GAA9B,CAAkCN,MAAlC,EAA0C,IAAIO,GAAJ,EAA1C;AACD;;AAED,QAAMV,uBAAuB,GAAGtB,KAAK,CAACsB,uBAAN,CAA8BoB,GAA9B,CAAkCjB,MAAlC,CAAhC;;AAEA,QAAI,CAACH,uBAAuB,CAACmB,GAAxB,CAA4BV,GAA5B,CAAL,EAAuC;AACrCT,6BAAuB,CAACqB,GAAxB,CAA4BZ,GAA5B;AACD;;AAED,WAAO,YAAM;AACXT,6BAAuB,CAACsB,MAAxB,CAA+Bb,GAA/B;;AACA,UAAI,CAACT,uBAAuB,CAACuB,IAA7B,EAAmC;AACjC7C,aAAK,CAACsB,uBAAN,CAA8BsB,MAA9B,CAAqCnB,MAArC;AACD;AACF,KALD;AAMD,GAjBQ,EAiBN,EAjBM,CAAT;AAmBA,SAAO,CAAE9B,KAAF,EAASK,KAAK,CAACG,QAAf,CAAP;AACD,C","file":"react-hookstore.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"react\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"React\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"hookStore\"] = factory(require(\"react\"));\n\telse\n\t\troot[\"hookStore\"] = factory(root[\"React\"]);\n})(typeof self !== 'undefined' ? self : this, function(__WEBPACK_EXTERNAL_MODULE__0__) {\nreturn "," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 1);\n","module.exports = __WEBPACK_EXTERNAL_MODULE__0__;","import { useState, useEffect } from 'react';\n\nlet stores = {};\nlet subscriptions = {};\n\nconst defaultReducer = (state, payload) => payload;\nconst defaultMemoFn = (state) => state;\n\n/** The public interface of a store */\nclass StoreInterface {\n constructor(name, store, useReducer) {\n this.name = name;\n useReducer ?\n this.dispatch = store.setState : this.setState = store.setState;\n this.getState = () => store.state;\n this.subscribe = this.subscribe.bind(this);\n }\n\n /**\n * Subscribe to store changes\n * @param {(state:any, data:any) => void} callback - The function to be invoked everytime the store is updated\n * @return {Function} - Call the function returned by the method to cancel the subscription\n */\n subscribe(callback) {\n if (!callback || typeof callback !== 'function') {\n throw `store.subscribe callback argument must be a function. got '${typeof callback}' instead.`;\n }\n if (subscriptions[this.name].find(c => c === callback)) {\n console.warn('This callback is already subscribed to this store. skipping subscription');\n return;\n }\n subscriptions[this.name].push(callback);\n return () => {\n subscriptions[this.name] = subscriptions[this.name].filter(c => c !== callback);\n }\n }\n\n /**\n * Set the store state\n * @param {any} data - The new state value.\n */\n setState(data) {\n console.warn(`[React Hookstore] Store ${this.name} uses a reducer to handle its state updates. use dispatch instead of setState`)\n }\n /**\n * Dispatch data to the store reducer\n * @param {any} data - The data payload the reducer receives\n */\n dispatch(data) {\n console.warn(`[React Hookstore] Store ${this.name} does not use a reducer to handle state updates. use setState instead of dispatch`)\n }\n}\n\nfunction getStoreByIdentifier(identifier) {\n const name = identifier instanceof StoreInterface ? identifier.name : identifier;\n if (!stores[name]) {\n throw `Store with name ${name} does not exist`;\n }\n return stores[name];\n}\n\n/**\n * Creates a new store\n * @param {String} name - The store namespace.\n * @param {*} state [{}] - The store initial state. It can be of any type.\n * @param {(state:any, data:any) => any} reducer [null] - The reducer handler. Optional\n * @returns {StoreInterface} The store instance.\n */\nexport function createStore(name, state = {}, reducer=defaultReducer) {\n if (typeof name !== 'string') {\n throw 'Store name must be a string';\n }\n if (stores[name]) {\n throw `Store with name ${name} already exists`;\n }\n \n\n const store = {\n state,\n reducer,\n setState(data, callback) {\n const isPrimitiveStateWithoutReducerAndIsPreviousState =\n this.reducer === defaultReducer\n && data === this.state\n && typeof data !== 'object';\n\n if (isPrimitiveStateWithoutReducerAndIsPreviousState) {\n if (typeof callback === 'function') callback(this.state)\n return;\n }\n\n const currentState = this.state;\n const newState = this.reducer(this.state, data);\n this.state = newState;\n\n this.updatersPerMemoFunction.forEach((updaters, memoFn) => {\n const prevResult = memoFn(currentState);\n const newResult = memoFn(newState);\n if (prevResult === newResult) {\n return;\n }\n for (let updateComponent of updaters) {\n updateComponent(this.state);\n }\n });\n\n if (subscriptions[name].length) {\n subscriptions[name].forEach(c => c(this.state, data));\n }\n\n if (typeof callback === 'function') callback(this.state)\n },\n updatersPerMemoFunction: new Map(),\n };\n\n store.setState = store.setState.bind(store);\n store.updatersPerMemoFunction.set(defaultMemoFn, new Set())\n stores = Object.assign({}, stores, { [name]: store });\n subscriptions[name] = [];\n\n store.public = new StoreInterface(name, store, reducer !== defaultReducer);\n return store.public;\n}\n\n/**\n * Returns a store instance based on its name\n * @name {String} name - The name of the wanted store\n * @returns {StoreInterface} the store instance\n */\nexport function getStoreByName(name) {\n try {\n return stores[name].public;\n } catch(e) {\n throw `Store with name ${name} does not exist`;\n }\n}\n\n/**\n * Returns a [ state, setState ] pair for the selected store. Can only be called within React Components\n * @param {String|StoreInterface} identifier - The identifier for the wanted store\n * @param {(state:any) => any} memoFn [state => state] - A memoization function to optimize component rerender. Receive the store state and return a subset of it. The component will only rerender when the subset changes.\n * @returns {Array} the [state, setState] pair.\n */\nexport function useStore(identifier, memoFn=defaultMemoFn) {\n const store = getStoreByIdentifier(identifier);\n if (!store) {\n throw 'store does not exist';\n }\n if (typeof memoFn !== 'function') {\n throw 'memoFn must be a function';\n }\n\n const [ state, set ] = useState(store.state);\n\n useEffect(() => {\n if (!store.updatersPerMemoFunction.has(memoFn)) {\n store.updatersPerMemoFunction.set(memoFn, new Set());\n }\n \n const updatersPerMemoFunction = store.updatersPerMemoFunction.get(memoFn);\n\n if (!updatersPerMemoFunction.has(set)) {\n updatersPerMemoFunction.add(set);\n }\n\n return () => {\n updatersPerMemoFunction.delete(set);\n if (!updatersPerMemoFunction.size) {\n store.updatersPerMemoFunction.delete(memoFn);\n }\n }\n }, [])\n\n return [ state, store.setState];\n}\n"],"sourceRoot":""} \ No newline at end of file diff --git a/dist/react-hookstore.min.js b/dist/react-hookstore.min.js index 574d166..e8c21e0 100644 --- a/dist/react-hookstore.min.js +++ b/dist/react-hookstore.min.js @@ -1 +1 @@ -!function(t){var e={};function s(n){if(e[n])return e[n].exports;var r=e[n]={i:n,l:!1,exports:{}};return t[n].call(r.exports,r,r.exports,s),r.l=!0,r.exports}s.m=t,s.c=e,s.d=function(t,e,n){s.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},s.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},s.t=function(t,e){if(1&e&&(t=s(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(s.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)s.d(n,r,function(e){return t[e]}.bind(null,r));return n},s.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return s.d(e,"a",e),e},s.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},s.p="",s(s.s=1)}([function(t,e){t.exports=void 0},function(t,e,s){"use strict";s.r(e),s.d(e,"createStore",function(){return u}),s.d(e,"getStoreByName",function(){return c}),s.d(e,"useStore",function(){return f});var n=s(0);let r={},o={};const i=(t,e)=>e;class a{constructor(t,e,s){this.name=t,s?this.dispatch=e.setState:this.setState=e.setState,this.getState=(()=>e.state),this.subscribe=this.subscribe.bind(this)}subscribe(t){if(!t||"function"!=typeof t)throw`store.subscribe callback argument must be a function. got '${typeof t}' instead.`;if(!o[this.name].find(e=>e===t))return o[this.name].push(t),()=>{o[this.name]=o[this.name].filter(e=>e!==t)};console.warn("This callback is already subscribed to this store. skipping subscription")}setState(){console.warn(`[React Hookstore] Store ${this.name} uses a reducer to handle its state updates. use dispatch instead of setState`)}dispatch(){console.warn(`[React Hookstore] Store ${this.name} does not use a reducer to handle state updates. use setState instead of dispatch`)}}function u(t,e={},s=i){if("string"!=typeof t)throw"Store name must be a string";if(r[t])throw`Store with name ${t} already exists`;const n={state:e,reducer:s,setState(e,s){this.state=this.reducer(this.state,e),this.setters.forEach(t=>t(this.state)),o[t].length&&o[t].forEach(t=>t(this.state,e)),"function"==typeof s&&s(this.state)},setters:[]};return n.setState=n.setState.bind(n),o[t]=[],n.public=new a(t,n,s!==i),r=Object.assign({},r,{[t]:n}),n.public}function c(t){try{return r[t].public}catch(e){throw`Store with name ${t} does not exist`}}function f(t){const e=function(t){const e=t instanceof a?t.name:t;if(!r[e])throw`Store with name ${e} does not exist`;return r[e]}(t),[s,o]=Object(n.useState)(e.state);return Object(n.useEffect)(()=>(e.setters.includes(o)||e.setters.push(o),()=>{e.setters=e.setters.filter(t=>t!==o)}),[]),[s,e.setState]}}]); \ No newline at end of file +!function(t){var e={};function n(s){if(e[s])return e[s].exports;var r=e[s]={i:s,l:!1,exports:{}};return t[s].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=t,n.c=e,n.d=function(t,e,s){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:s})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var s=Object.create(null);if(n.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)n.d(s,r,function(e){return t[e]}.bind(null,r));return s},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=1)}([function(t,e){t.exports=void 0},function(t,e,n){"use strict";n.r(e),n.d(e,"createStore",function(){return c}),n.d(e,"getStoreByName",function(){return f}),n.d(e,"useStore",function(){return d});var s=n(0);let r={},o={};const i=(t,e)=>e,a=t=>t;class u{constructor(t,e,n){this.name=t,n?this.dispatch=e.setState:this.setState=e.setState,this.getState=(()=>e.state),this.subscribe=this.subscribe.bind(this)}subscribe(t){if(!t||"function"!=typeof t)throw`store.subscribe callback argument must be a function. got '${typeof t}' instead.`;if(!o[this.name].find(e=>e===t))return o[this.name].push(t),()=>{o[this.name]=o[this.name].filter(e=>e!==t)};console.warn("This callback is already subscribed to this store. skipping subscription")}setState(t){console.warn(`[React Hookstore] Store ${this.name} uses a reducer to handle its state updates. use dispatch instead of setState`)}dispatch(t){console.warn(`[React Hookstore] Store ${this.name} does not use a reducer to handle state updates. use setState instead of dispatch`)}}function c(t,e={},n=i){if("string"!=typeof t)throw"Store name must be a string";if(r[t])throw`Store with name ${t} already exists`;const s={state:e,reducer:n,setState(e,n){if(this.reducer===i&&e===this.state&&"object"!=typeof e)return void("function"==typeof n&&n(this.state));const s=this.state,r=this.reducer(this.state,e);this.state=r,this.updatersPerMemoFunction.forEach((t,e)=>{if(e(s)!==e(r))for(let e of t)e(this.state)}),o[t].length&&o[t].forEach(t=>t(this.state,e)),"function"==typeof n&&n(this.state)},updatersPerMemoFunction:new Map};return s.setState=s.setState.bind(s),s.updatersPerMemoFunction.set(a,new Set),r=Object.assign({},r,{[t]:s}),o[t]=[],s.public=new u(t,s,n!==i),s.public}function f(t){try{return r[t].public}catch(e){throw`Store with name ${t} does not exist`}}function d(t,e=a){const n=function(t){const e=t instanceof u?t.name:t;if(!r[e])throw`Store with name ${e} does not exist`;return r[e]}(t);if(!n)throw"store does not exist";if("function"!=typeof e)throw"memoFn must be a function";const[o,i]=Object(s.useState)(n.state);return Object(s.useEffect)(()=>{n.updatersPerMemoFunction.has(e)||n.updatersPerMemoFunction.set(e,new Set);const t=n.updatersPerMemoFunction.get(e);return t.has(i)||t.add(i),()=>{t.delete(i),t.size||n.updatersPerMemoFunction.delete(e)}},[]),[o,n.setState]}}]); \ No newline at end of file diff --git a/docs/demo.js b/docs/demo.js deleted file mode 100644 index ec4bddc..0000000 --- a/docs/demo.js +++ /dev/null @@ -1,32 +0,0 @@ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var l=t[r]={i:r,l:!1,exports:{}};return e[r].call(l.exports,l,l.exports,n),l.l=!0,l.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var l in e)n.d(r,l,function(t){return e[t]}.bind(null,l));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=8)}([function(e,t,n){"use strict";e.exports=n(3)},function(e,t,n){"use strict"; -/* -object-assign -(c) Sindre Sorhus -@license MIT -*/var r=Object.getOwnPropertySymbols,l=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable;e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map(function(e){return t[e]}).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach(function(e){r[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var n,a,i=function(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}(e),u=1;uz.length&&z.push(e)}function D(e,t,n){return null==e?0:function e(t,n,r,l){var i=typeof t;"undefined"!==i&&"boolean"!==i||(t=null);var u=!1;if(null===t)u=!0;else switch(i){case"string":case"number":u=!0;break;case"object":switch(t.$$typeof){case o:case a:u=!0}}if(u)return r(l,t,""===n?"."+U(t,0):n),1;if(u=0,n=""===n?".":n+":",Array.isArray(t))for(var c=0;cthis.eventPool.length&&this.eventPool.push(e)}function fe(e){e.eventPool=[],e.getPooled=ce,e.release=se}l(ue.prototype,{preventDefault:function(){this.defaultPrevented=!0;var e=this.nativeEvent;e&&(e.preventDefault?e.preventDefault():"unknown"!=typeof e.returnValue&&(e.returnValue=!1),this.isDefaultPrevented=ae)},stopPropagation:function(){var e=this.nativeEvent;e&&(e.stopPropagation?e.stopPropagation():"unknown"!=typeof e.cancelBubble&&(e.cancelBubble=!0),this.isPropagationStopped=ae)},persist:function(){this.isPersistent=ae},isPersistent:ie,destructor:function(){var e,t=this.constructor.Interface;for(e in t)this[e]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null,this.isPropagationStopped=this.isDefaultPrevented=ie,this._dispatchInstances=this._dispatchListeners=null}}),ue.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(e){return e.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null},ue.extend=function(e){function t(){}function n(){return r.apply(this,arguments)}var r=this;t.prototype=r.prototype;var o=new t;return l(o,n.prototype),n.prototype=o,n.prototype.constructor=n,n.Interface=l({},r.Interface,e),n.extend=r.extend,fe(n),n},fe(ue);var de=ue.extend({data:null}),pe=ue.extend({data:null}),me=[9,13,27,32],he=$&&"CompositionEvent"in window,ye=null;$&&"documentMode"in document&&(ye=document.documentMode);var ve=$&&"TextEvent"in window&&!ye,ge=$&&(!he||ye&&8=ye),be=String.fromCharCode(32),ke={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},we=!1;function xe(e,t){switch(e){case"keyup":return-1!==me.indexOf(t.keyCode);case"keydown":return 229!==t.keyCode;case"keypress":case"mousedown":case"blur":return!0;default:return!1}}function Te(e){return"object"==typeof(e=e.detail)&&"data"in e?e.data:null}var Se=!1;var Ee={eventTypes:ke,extractEvents:function(e,t,n,r){var l=void 0,o=void 0;if(he)e:{switch(e){case"compositionstart":l=ke.compositionStart;break e;case"compositionend":l=ke.compositionEnd;break e;case"compositionupdate":l=ke.compositionUpdate;break e}l=void 0}else Se?xe(e,n)&&(l=ke.compositionEnd):"keydown"===e&&229===n.keyCode&&(l=ke.compositionStart);return l?(ge&&"ko"!==n.locale&&(Se||l!==ke.compositionStart?l===ke.compositionEnd&&Se&&(o=oe()):(re="value"in(ne=r)?ne.value:ne.textContent,Se=!0)),l=de.getPooled(l,t,n,r),o?l.data=o:null!==(o=Te(n))&&(l.data=o),H(l),o=l):o=null,(e=ve?function(e,t){switch(e){case"compositionend":return Te(t);case"keypress":return 32!==t.which?null:(we=!0,be);case"textInput":return(e=t.data)===be&&we?null:e;default:return null}}(e,n):function(e,t){if(Se)return"compositionend"===e||!he&&xe(e,t)?(e=oe(),le=re=ne=null,Se=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1