diff --git a/package-lock.json b/package-lock.json index a0d4ad4..e477249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cobuildlab/react-simple-state", - "version": "0.6.0", + "version": "0.6.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@cobuildlab/react-simple-state", - "version": "0.6.0", + "version": "0.6.2", "license": "GPL-3.0", "devDependencies": { "@babel/core": "^7.9.6", @@ -29,15 +29,15 @@ "jest": "^26.0.1", "lint-staged": "^10.2.2", "prettier": "^2.0.5", - "react": "^16.13.1", + "react": "17.0.2", "react-test-renderer": "^16.13.1", "ts-jest": "^25.5.1", "typedoc": "^0.17.8", "typedoc-plugin-markdown": "^2.3.1", - "typescript": "^4.2.2" + "typescript": "^4.4.2" }, "peerDependencies": { - "react": "^16.8.6" + "react": "^17.0.2" } }, "node_modules/@babel/code-frame": { @@ -10049,14 +10049,13 @@ "dev": true }, "node_modules/react": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", - "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "dev": true, "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "object-assign": "^4.1.1" }, "engines": { "node": ">=0.10.0" @@ -11658,9 +11657,9 @@ } }, "node_modules/typescript": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.2.tgz", - "integrity": "sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", + "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -20469,14 +20468,13 @@ "dev": true }, "react": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", - "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "dev": true, "requires": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "object-assign": "^4.1.1" } }, "react-is": { @@ -21790,9 +21788,9 @@ } }, "typescript": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.2.tgz", - "integrity": "sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", + "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index c3d41fc..4b29baa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cobuildlab/react-simple-state", - "version": "0.6.2", + "version": "0.7.0", "description": "Simple and Lightweight state management for react applications. ", "main": "lib/index.js", "types": "./lib/index.d.ts", @@ -39,15 +39,23 @@ "jest": "^26.0.1", "lint-staged": "^10.2.2", "prettier": "^2.0.5", - "react": "^16.13.1", + "react": "17.0.2", "react-test-renderer": "^16.13.1", "ts-jest": "^25.5.1", "typedoc": "^0.17.8", "typedoc-plugin-markdown": "^2.3.1", - "typescript": "^4.2.2" + "typescript": "^4.4.2" }, "peerDependencies": { - "react": "^16.8.6" + "react": ">=16.13.1" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } }, "husky": { "hooks": { diff --git a/src/actions.ts b/src/actions.ts index 9105d4a..cc68322 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -20,8 +20,10 @@ export function createAction( try { data = await action(...params); } catch (error) { - errorEvent.dispatch(error); - return { error }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + errorEvent.dispatch(error as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { error } as { error: any }; } event.dispatch(data); diff --git a/src/index.ts b/src/index.ts index f6f0716..6967a51 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,6 @@ export * from './hooks'; export * from './views'; export * from './event'; export * from './actions'; +export * from './store'; +export * from './store-hooks'; +export * from './store-utils'; diff --git a/src/pub-sub.ts b/src/pub-sub.ts index 44cf8f9..6f42e0a 100644 --- a/src/pub-sub.ts +++ b/src/pub-sub.ts @@ -3,7 +3,7 @@ export interface Subscription { } export interface Subscriber { - update: (value: T | null) => void; + update: (value: T) => void; } export interface Publisher { @@ -11,7 +11,7 @@ export interface Publisher { subscribe(subscriber: Subscriber): Subscription; - notify(value: T | null): void; + notify(value: T): void; } /** @@ -53,7 +53,7 @@ class ConcretePublisher implements Publisher { return new ConcreteSubscription(this, subscriber); } - public notify(value: T | null): void { + public notify(value: T): void { for (const subscriber of this.subscribers) { subscriber.update(value); } diff --git a/src/store-hooks.ts b/src/store-hooks.ts new file mode 100644 index 0000000..2807501 --- /dev/null +++ b/src/store-hooks.ts @@ -0,0 +1,88 @@ +import { useEffect, useRef, useState } from 'react'; +import { Store } from './store'; + +/** + * @param {Store} store - Store to subscribe. + * @param {Function} callback - Function to call on each dipatch. + */ +export function useStoreSubcription( + store: Store, + callback: (data: T) => void, +): void { + const callbacksRef = useRef({ + callback, + }); + + callbacksRef.current = { + callback, + }; + + useEffect(() => { + const unsubscribeSuccess = store.subscribe((data) => { + if (callbacksRef.current.callback) { + callbacksRef.current.callback(data); + } + }); + + return () => { + unsubscribeSuccess.unsubscribe(); + }; + }, [store]); +} + +/** + * @param {Store} store - Store to subscribe. + * @param {Function} errorcCallback - Function to call on each error dipatch. + */ +export function useStoreErrorSubscription( + store: Store, + errorcCallback: (data: Error) => void, +): void { + const callbacksRef = useRef({ + errorcCallback, + }); + + callbacksRef.current = { + errorcCallback, + }; + + useEffect(() => { + const unsubscribeError = store.subscribeError((data) => { + if (callbacksRef.current.errorcCallback) { + callbacksRef.current.errorcCallback(data); + } + }); + + return () => { + unsubscribeError.unsubscribe(); + }; + }, [store]); +} + +/** + * @param {Store} store - Store to subscribe. + * @returns {Object} - Resulto object from the store. + */ +export function useStore(store: Store): T { + const [state, setState] = useState(store.get()); + + useStoreSubcription(store, (data) => { + setState(data); + }); + + return state; +} + +/** + * @param {Store} store - Store to subscribe. + * @returns {Object} - Resulto object from the store. + */ +export function useStoreError(store: Store): Error | null { + const [state, setState] = useState(null); + + useStoreErrorSubscription(store, (data) => { + setState(data as Error); + }); + + return state; +} diff --git a/src/store-utils.ts b/src/store-utils.ts new file mode 100644 index 0000000..6dbef99 --- /dev/null +++ b/src/store-utils.ts @@ -0,0 +1,34 @@ +import { CheckDispatchType, Store } from './store'; + +/** + * @param {Store} store - Event. + * @param {Function} callback - Callback. + * @param {Function} sideEffect - Callback. + * @returns {Function} Reducer fucntion. + */ +export function createStoreAction( + store: Store, + callback: + | ((prevState: T, ...params: V) => CheckDispatchType) + | ((prevState: T, ...params: V) => Promise>), + sideEffect?: (...params: V) => void, +) { + return (...params: V): void => { + if (sideEffect) { + sideEffect(...params); + } + const result = callback(store.get(), ...params); + + if (result instanceof Promise) { + result + .then((data) => { + store.dispatch(data); + }) + .catch((e) => { + store.dispatchError(e); + }); + return; + } + store.dispatch(result); + }; +} diff --git a/src/store.ts b/src/store.ts index 0d88567..d415317 100644 --- a/src/store.ts +++ b/src/store.ts @@ -4,27 +4,30 @@ import { Subscriber, Subscription, } from './pub-sub'; -import { Reducer } from './event'; -export type EventParams = { - initialValue?: T | null; - reducer: Reducer; - stores: [...U]; +type Reducer = (prevState: T, newState: R) => T; +export type CheckDispatchType = R extends unknown ? T : R; + +export type StoreParams = { + initialValue: T; + reducer?: Reducer; }; -export class Store { - private value: T | null = null; - private readonly reducer?: Reducer; +export class Store { + private value: T; + private initialValue: T; private publisher: Publisher = new ConcretePublisher(); private errorPublisher: Publisher = new ConcretePublisher(); + private reducer: Reducer | undefined; + + constructor(eventDescriptor: StoreParams) { + this.value = eventDescriptor.initialValue; + this.initialValue = eventDescriptor.initialValue; - constructor(eventDescriptor?: EventParams) { - if (eventDescriptor && eventDescriptor.initialValue) - this.value = eventDescriptor.initialValue; - this.reducer = eventDescriptor?.reducer; + this.reducer = eventDescriptor.reducer; } subscribe( - subscriber: (value: T | null) => void, + subscriber: (value: T) => void, receiveLastValue = false, ): Subscription { const _subscriber: Subscriber = { @@ -34,27 +37,28 @@ export class Store { return this.publisher.subscribe(_subscriber); } - subscribeError(subscriber: (value: Error | null) => void): Subscription { + subscribeError(subscriber: (value: Error) => void): Subscription { const _subscriber: Subscriber = { update: subscriber, }; return this.errorPublisher.subscribe(_subscriber); } - dispatch(eventValue: T | U | null): void { - const value = Object.freeze( - this.reducer !== null && this.reducer !== undefined - ? this.reducer(eventValue as U) - : (eventValue as T), - ); + dispatch(eventValue: CheckDispatchType): void { + const value = this.reducer + ? this.reducer(this.value, eventValue as R) + : this.value; this.value = value; - this.publisher.notify(value); + + this.publisher.notify(Object.freeze(value)); } + dispatchError(value: Error): void { this.errorPublisher.notify(value); } - get(): T | null { + + get(): T { return Object.freeze(this.value); } @@ -64,10 +68,10 @@ export class Store { * @param {boolean} dispatch - */ clear(dispatch = false): void { + this.value = this.initialValue; + if (dispatch) { - this.dispatch(null); // Empty dispatch - } else { - this.value = null; + this.dispatch(this.value); // Empty dispatch } } }