From 50482f77c77d702e1915035e04c9dd96a68885db Mon Sep 17 00:00:00 2001 From: "Sergey S. Volkov" Date: Sat, 16 May 2020 19:07:14 +0300 Subject: [PATCH] [new packages] - @reffect/strict @reffect/undoable + [new version] - @reffect/react 1.1.3 (#9) * [new package] @reffect/undoable (#7) * feat: initial commit for reffect-undoable package, add simplest implementation undo redo * feat: create simple undo redo store extension package * chore: change build config (output plugins) * chore: remove build files * chore: add gitignore for reffect/undoable * chore: lock file update * feat: rewrite undoable to reffect store with couple effects (undo, redo) * chore: up size limits for reffect undoable package; chore: change keywords for undoable * docs: update readme for monorepo (add Install section); docs: add readme for undoable package * [new package] @reffect/strict (#8) * feat: add the "strict" package; fix(@reffect/react): problems with unmounted react hooks * bump: @reffect/react to 1.1.3 * docs: update README for strict * chore: set initial version for strict package * docs: update README , CHANGELOG * docs: fix badge in new packages READMEs --- CHANGELOG.md | 4 +- README.md | 20 +++++ build/utils/createBuildConfig.js | 35 +++++---- packages/react/.size-limit.json | 6 +- packages/react/CHANGELOG.md | 6 ++ packages/react/package.json | 2 +- packages/react/src/useEffectState.ts | 9 ++- packages/react/src/useStore.ts | 9 ++- packages/strict/.gitignore | 4 + packages/strict/.size-limit.json | 17 +++++ packages/strict/CHANGELOG.md | 3 + packages/strict/README.md | 29 ++++++++ packages/strict/package.json | 48 ++++++++++++ packages/strict/src/index.ts | 29 ++++++++ packages/undoable/.gitignore | 4 + packages/undoable/.size-limit.json | 17 +++++ packages/undoable/README.md | 106 +++++++++++++++++++++++++++ packages/undoable/package.json | 50 +++++++++++++ packages/undoable/src/index.ts | 68 +++++++++++++++++ 19 files changed, 444 insertions(+), 22 deletions(-) create mode 100644 packages/strict/.gitignore create mode 100644 packages/strict/.size-limit.json create mode 100644 packages/strict/CHANGELOG.md create mode 100644 packages/strict/README.md create mode 100644 packages/strict/package.json create mode 100644 packages/strict/src/index.ts create mode 100644 packages/undoable/.gitignore create mode 100644 packages/undoable/.size-limit.json create mode 100644 packages/undoable/README.md create mode 100644 packages/undoable/package.json create mode 100644 packages/undoable/src/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dbdf21..a106e39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,4 +2,6 @@ Changelogs: [**@reffect/core**](./packages/core/CHANGELOG.md) [**@reffect/localstore**](./packages/localstore/CHANGELOG.md) [**@reffect/logger**](./packages/logger/CHANGELOG.md) -[**@reffect/react**](./packages/react/CHANGELOG.md) +[**@reffect/react**](./packages/react/CHANGELOG.md) +[**@reffect/undoable**](./packages/undoable/CHANGELOG.md) +[**@reffect/strict**](./packages/strict/CHANGELOG.md) diff --git a/README.md b/README.md index 226f3ad..cb33db3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,26 @@ Reffect — is a declarative and reactive multi-store state manager for JavaScri [![npm](https://img.shields.io/npm/v/@reffect/logger?style=flat-square&color=blue)](https://www.npmjs.com/package/@reffect/logger) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@reffect/logger?style=flat-square&color=blue)](https://bundlephobia.com/result?p=@reffect/logger) - [`@reffect/localstore`](https://github.com/acacode/reffect/tree/master/packages/localstore) - store middleware to synchronize store with local storage key [![npm](https://img.shields.io/npm/v/@reffect/localstore?style=flat-square&color=blue)](https://www.npmjs.com/package/@reffect/localstore) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@reffect/localstore?style=flat-square&color=blue)](https://bundlephobia.com/result?p=@reffect/localstore) +- [`@reffect/undoable`](https://github.com/acacode/reffect/tree/master/packages/undoable) - store extension which provides undo/redo effects and store history + [![npm](https://img.shields.io/npm/v/@reffect/undoable?style=flat-square&color=blue)](https://www.npmjs.com/package/@reffect/undoable) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@reffect/undoable?style=flat-square&color=blue)](https://bundlephobia.com/result?p=@reffect/undoable) +- [`@reffect/strict`](https://github.com/acacode/reffect/tree/master/packages/strict) - store middleware for making store updates more strict + [![npm](https://img.shields.io/npm/v/@reffect/strict?style=flat-square&color=blue)](https://www.npmjs.com/package/@reffect/strict) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@reffect/strict?style=flat-square&color=blue)](https://bundlephobia.com/result?p=@reffect/strict) + +## Install + +Before at all you need to install main package: + +```bash +$ npm i -S @reffect/core +# or using yarn +$ yarn install @reffect/core +``` + +If project is using [React](https://github.com/facebook/react) you need to install [@reffect/react](https://github.com/acacode/reffect/tree/master/packages/react) (pack of React hooks which simplify usage with React application) + +```bash +$ npm i -S @reffect/react +``` ## Examples diff --git a/build/utils/createBuildConfig.js b/build/utils/createBuildConfig.js index 3e172f7..9b1e2e7 100644 --- a/build/utils/createBuildConfig.js +++ b/build/utils/createBuildConfig.js @@ -32,25 +32,28 @@ const createBuildConfig = ({ output: { file: outputPathToFile, format: outputFormat, - sourcemap, + sourcemap: sourcemap, + plugins: [ + defaultPlugins && + terser({ + sourcemap: sourcemap, + compress: { + pure_getters: true, + unsafe: true, + unsafe_comps: true, + warnings: false, + arguments: true, + unsafe_Function: true, + module: true, + passes: 30 + } + }) + ].filter(Boolean), ...commonOutput }, external: id => externalDeps.includes(id), context: projectPath, plugins: [ - defaultPlugins && - terser({ - compress: { - pure_getters: true, - unsafe: true, - unsafe_comps: true, - warnings: false, - arguments: true, - unsafe_Function: true, - module: true, - passes: 15 - } - }), defaultPlugins && typescript({ allowJs: false, @@ -64,7 +67,9 @@ const createBuildConfig = ({ noImplicitReturns: true, noImplicitThis: true, noUnusedLocals: true, - sourceMap: false, + inlineSourceMap: true, + inlineSources: true, + sourceMap: true, strictNullChecks: true, suppressImplicitAnyIndexErrors: true, exclude: ["node_modules", "**/*.spec.ts", "**/*.test.ts"] diff --git a/packages/react/.size-limit.json b/packages/react/.size-limit.json index ff9958f..2df3c2a 100644 --- a/packages/react/.size-limit.json +++ b/packages/react/.size-limit.json @@ -2,16 +2,16 @@ { "path": "reffect-react.es.js", "webpack": false, - "limit": "300 B" + "limit": "335 B" }, { "path": "reffect-react.cjs.js", "webpack": false, - "limit": "350 B" + "limit": "355 B" }, { "path": "reffect-react.umd.js", "webpack": false, - "limit": "450 B" + "limit": "455 B" } ] diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 9abff81..c786986 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.1.3 + +### Fix: + +1. update hooks states only if hook is mounted + # 1.1.2 ### Minor: diff --git a/packages/react/package.json b/packages/react/package.json index de0b912..41d1808 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@reffect/react", - "version": "1.1.2", + "version": "1.1.3", "description": "React bindings for Reffect", "scripts": { "build": "node ../../build/build.js", diff --git a/packages/react/src/useEffectState.ts b/packages/react/src/useEffectState.ts index 0288c06..0b77177 100644 --- a/packages/react/src/useEffectState.ts +++ b/packages/react/src/useEffectState.ts @@ -5,7 +5,14 @@ import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; export const useEffectState = (effect: Effect) => { const [state, setState] = useState(null); - useIsomorphicLayoutEffect(() => manage(effect).subscribe(state => setState(state)), []); + useIsomorphicLayoutEffect(() => { + let isMount = true; + const unsubscribe = manage(effect).subscribe(state => isMount && setState(state)); + return () => { + isMount = false; + unsubscribe(); + }; + }, []); return { pending: state === "pending", diff --git a/packages/react/src/useStore.ts b/packages/react/src/useStore.ts index 49260b4..e7f38c2 100644 --- a/packages/react/src/useStore.ts +++ b/packages/react/src/useStore.ts @@ -7,7 +7,14 @@ const reducer = (state: any, newState: any) => ({ ...newState }); export const useStore = (store: Store): Store => { const [state, setState] = useReducer(reducer, store); - useIsomorphicLayoutEffect(() => manage(store).subscribe(() => setState(store)), []); + useIsomorphicLayoutEffect(() => { + let isMount = true; + const unsubscribe = manage(store).subscribe(() => isMount && setState(store)); + return () => { + isMount = false; + unsubscribe(); + }; + }, []); return state; }; diff --git a/packages/strict/.gitignore b/packages/strict/.gitignore new file mode 100644 index 0000000..49baf40 --- /dev/null +++ b/packages/strict/.gitignore @@ -0,0 +1,4 @@ +reffect-strict.*.js +reffect-strict.d.ts +reffect-strict.*.js.map +package-lock.json \ No newline at end of file diff --git a/packages/strict/.size-limit.json b/packages/strict/.size-limit.json new file mode 100644 index 0000000..c6d481d --- /dev/null +++ b/packages/strict/.size-limit.json @@ -0,0 +1,17 @@ +[ + { + "path": "reffect-strict.es.js", + "webpack": false, + "limit": "310 B" + }, + { + "path": "reffect-strict.cjs.js", + "webpack": false, + "limit": "340 B" + }, + { + "path": "reffect-strict.umd.js", + "webpack": false, + "limit": "450 B" + } +] diff --git a/packages/strict/CHANGELOG.md b/packages/strict/CHANGELOG.md new file mode 100644 index 0000000..323047f --- /dev/null +++ b/packages/strict/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.0.1 + +Created project diff --git a/packages/strict/README.md b/packages/strict/README.md new file mode 100644 index 0000000..aa6eabb --- /dev/null +++ b/packages/strict/README.md @@ -0,0 +1,29 @@ +
+ +[![reffect logo](https://raw.githubusercontent.com/acacode/reffect/master/assets/reffect.png)](https://github.com/acacode/reffect) +[![npm](https://img.shields.io/npm/v/@reffect/strict?style=flat-square&color=blue)](https://www.npmjs.com/package/@reffect/strict) +[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@reffect/strict?style=flat-square&color=blue)](https://bundlephobia.com/result?p=@reffect/strict) +[![license](https://img.shields.io/github/license/acacode/reffect?style=flat-square&color=blue)](https://github.com/acacode/reffect) + +
+ +Reffect — is a declarative and reactive multi-store state manager for JavaScript/TypeScript applications inspired by [Reatom](https://github.com/artalar/reatom) and [Effector](https://github.com/zerobias/effector) + +# @reffect/strict + +Store middleware for [`Reffect`](https://github.com/acacode/reffect) + +## How to use + +```ts +import { store, effect } from "@reffect/core"; +import { strictUpdate } from "@reffect/logger"; + +const projectsStore = store({ projects: [] }, "projects", [strictUpdate]); + +const setProjects = effect(projectsStore, "projects"); +// ... + +setProjects(["foo", "bar"]); // state of projectsStore will update to { projects: ["foo", "bar"] } +setProjects(["foo", "bar"]); // state of projectsStore won't update because new state and current are equals +``` diff --git a/packages/strict/package.json b/packages/strict/package.json new file mode 100644 index 0000000..fbd729c --- /dev/null +++ b/packages/strict/package.json @@ -0,0 +1,48 @@ +{ + "name": "@reffect/strict", + "version": "0.0.1", + "description": "strict update store middleware for Reffect", + "scripts": { + "build": "node ../../build/build.js", + "size": "size-limit" + }, + "author": "acacode", + "license": "MIT", + "readme": "README.md", + "private": false, + "sideEffects": false, + "main": "reffect-strict.cjs.js", + "typings": "reffect-strict.d.ts", + "umd:main": "reffect-strict.umd.js", + "jsnext:main": "reffect-strict.es.js", + "module": "reffect-strict.es.js", + "peerDependencies": { "@reffect/core": "^1.5.0" }, + "devDependencies": { + "@reffect/core": "^1.5.0", + "@size-limit/preset-small-lib": "^4.2.1", + "size-limit": "^4.2.1" + }, + "files": [ + "LICENSE", + "reffect-strict.d.ts", + "reffect-strict.cjs.js", + "reffect-strict.cjs.js.map", + "reffect-strict.es.js", + "reffect-strict.es.js.map", + "reffect-strict.umd.js", + "reffect-strict.umd.js.map" + ], + "keywords": [ + "redux", + "effector", + "mobx", + "state", + "store", + "flux", + "reactive", + "state-manager", + "state manager", + "reffect" + ], + "gitHead": "a724ce9d6d802cf83fec609ad4664172a4694471" +} \ No newline at end of file diff --git a/packages/strict/src/index.ts b/packages/strict/src/index.ts new file mode 100644 index 0000000..eed9842 --- /dev/null +++ b/packages/strict/src/index.ts @@ -0,0 +1,29 @@ +import { StoreType, manage } from "@reffect/core"; + +const equals = (obj1: object, obj2: object): obj1 is typeof obj2 => { + if (obj1 === obj2) return true; + if (!(obj1 instanceof Object) || !(obj2 instanceof Object)) return false; + if (obj1.constructor !== obj2.constructor) return false; + + for (const property in obj1) { + if (obj1.hasOwnProperty(property) && (!obj2.hasOwnProperty(property) || !equals(obj1[property], obj2[property]))) + return false; + } + + for (const property in obj2) { + if (obj2.hasOwnProperty(property) && !obj1.hasOwnProperty(property)) return false; + } + return true; +}; + +export const strictUpdate = (store: Store, copy: (obj: object) => object) => { + const storeManager = manage(store); + const originalPartialUpdate = storeManager.partialUpdate; + + storeManager.partialUpdate = (storeUpdate: Partial) => + storeUpdate && + !equals(store, copy({ ...storeManager.state, ...storeUpdate })) && + originalPartialUpdate(storeUpdate); + + return store; +}; diff --git a/packages/undoable/.gitignore b/packages/undoable/.gitignore new file mode 100644 index 0000000..3cb55ca --- /dev/null +++ b/packages/undoable/.gitignore @@ -0,0 +1,4 @@ +reffect-undoable.*.js +reffect-undoable.d.ts +reffect-undoable.*.js.map +package-lock.json \ No newline at end of file diff --git a/packages/undoable/.size-limit.json b/packages/undoable/.size-limit.json new file mode 100644 index 0000000..4b59c82 --- /dev/null +++ b/packages/undoable/.size-limit.json @@ -0,0 +1,17 @@ +[ + { + "path": "reffect-undoable.es.js", + "webpack": false, + "limit": "410 B" + }, + { + "path": "reffect-undoable.cjs.js", + "webpack": false, + "limit": "450 B" + }, + { + "path": "reffect-undoable.umd.js", + "webpack": false, + "limit": "550 B" + } +] diff --git a/packages/undoable/README.md b/packages/undoable/README.md new file mode 100644 index 0000000..25264ad --- /dev/null +++ b/packages/undoable/README.md @@ -0,0 +1,106 @@ +
+ +[![reffect logo](https://raw.githubusercontent.com/acacode/reffect/master/assets/reffect.png)](https://github.com/acacode/reffect) +[![npm](https://img.shields.io/npm/v/@reffect/undoable?style=flat-square&color=blue)](https://www.npmjs.com/package/@reffect/undoable) +[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@reffect/undoable?style=flat-square&color=blue)](https://bundlephobia.com/result?p=@reffect/undoable) +[![license](https://img.shields.io/github/license/acacode/reffect?style=flat-square&color=blue)](https://github.com/acacode/reffect) + +
+ +Reffect — is a declarative and reactive multi-store state manager for JavaScript/TypeScript applications inspired by [Reatom](https://github.com/artalar/reatom) and [Effector](https://github.com/zerobias/effector) + +# @reffect/undoable + +Extension for [`Reffect`](https://github.com/acacode/reffect) stores. +Provides undo/redo effects and store history. + +## How to use + +Import `undoable` function from package: + +```ts +import { undoable } from "@reffect/undoable"; +``` + +Call `undoable` and send to it your store at first argument: + +```ts +import { store } from "@reffect/core"; +const storeRef = store({ foo: "bar" }); +const { history, undo, redo } = undoable(storeRef, middlewares, limit); +// `middlewares` it is array of reffect store middlewares +// `limit` means limit for state history +``` + +### undo (`VoidFunction`) + +This function move state of wrapped store to the previous value + +```ts +undo(); +``` + +### redo (`VoidFunction`) + +This function move state of wrapped store to the next value (if `undo()` was called early) + +```ts +redo(); +``` + +### history (`{ past: Store[]; present: Store; future: Store[]; }`) + +It's reffect store which have state history of wrapped store + +```ts +import { manage } from "@reffect/core"; + +manage(history).subscribe((payload, prevState, currState) => console.log(payload, prevState, currState)); +``` + +## Examples + +```ts +import { store, effect } from "@reffect/core"; +import { undoable } from "@reffect/undoable"; + +const keyboards = store({ list: [] }); +const { history, undo, redo } = undoable(keyboards); + +const addKeyboard = effect(keyboards, name => ({ list: [...keyboards.list, name] })); + +addKeyboard("Das Keyboard 4Q"); +addKeyboard("Leopold FC900R"); +addKeyboard("Leopold FC750R"); + +console.log(keyboards.list); // ["Das Keyboard 4Q", "Leopold FC900R", "Leopold FC750R"] +undo(); +console.log(keyboards.list); // ["Das Keyboard 4Q", "Leopold FC900R"] +undo(); +console.log(keyboards.list); // ["Das Keyboard 4Q"] +redo(); +console.log(keyboards.list); // ["Das Keyboard 4Q", "Leopold FC900R"] +redo(); +console.log(keyboards.list); // ["Das Keyboard 4Q", "Leopold FC900R", "Leopold FC750R"] + +console.log(history); +/* + { + past: [ + { + list: [], + }, + { + list: ["Das Keyboard 4Q"], + }, + { + list: ["Das Keyboard 4Q", "Leopold FC900R"], + }, + ], + present: { + list: ["Das Keyboard 4Q", "Leopold FC900R", "Leopold FC750R"] + }, + future: [] + } +*/ +``` diff --git a/packages/undoable/package.json b/packages/undoable/package.json new file mode 100644 index 0000000..0058904 --- /dev/null +++ b/packages/undoable/package.json @@ -0,0 +1,50 @@ +{ + "name": "@reffect/undoable", + "version": "0.0.1", + "description": "Reffect stores extension", + "scripts": { + "build": "node ../../build/build.js", + "size": "size-limit" + }, + "author": "acacode", + "license": "MIT", + "readme": "README.md", + "private": false, + "sideEffects": false, + "main": "reffect-undoable.cjs.js", + "typings": "reffect-undoable.d.ts", + "umd:main": "reffect-undoable.umd.js", + "jsnext:main": "reffect-undoable.es.js", + "module": "reffect-undoable.es.js", + "peerDependencies": { "@reffect/core": "^1.5.0" }, + "devDependencies": { + "@reffect/core": "^1.5.0", + "@size-limit/preset-small-lib": "^4.2.1", + "size-limit": "^4.2.1" + }, + "files": [ + "LICENSE", + "reffect-undoable.d.ts", + "reffect-undoable.cjs.js", + "reffect-undoable.cjs.js.map", + "reffect-undoable.es.js", + "reffect-undoable.es.js.map", + "reffect-undoable.umd.js", + "reffect-undoable.umd.js.map" + ], + "keywords": [ + "redux", + "effector", + "mobx", + "state", + "store", + "flux", + "reactive", + "state-manager", + "state manager", + "undo", + "undoable", + "reffect" + ], + "gitHead": "7636d0e5d6b6ba2f17c0b415f0761b559ae2a20c" +} \ No newline at end of file diff --git a/packages/undoable/src/index.ts b/packages/undoable/src/index.ts new file mode 100644 index 0000000..f6e6623 --- /dev/null +++ b/packages/undoable/src/index.ts @@ -0,0 +1,68 @@ +import { StoreType, manage, store, effect, StoreMiddleware } from "@reffect/core"; + +type Undoable = { + history: StoreHistory; + undo: VoidFunction; + redo: VoidFunction; +}; + +export type StoreHistory = { + past: Store[]; + present: Store; + future: Store[]; +}; + +export const undoable = ( + storeRef: Store, + middlewares?: StoreMiddleware>[], + limit = 15, +): Undoable => { + let updateFromUndoable = false; + const storeRefManager = manage(storeRef); + const storeHistory = store>( + { + past: [], + present: storeRefManager.initialState as Store, + future: [], + }, + `${storeRefManager.name}-history`, + middlewares, + ); + + const undoRedo = (isRedo?: boolean): Partial> | void => { + const historyArray = storeHistory[isRedo ? "future" : "past"]; + if (historyArray.length) { + const sliced = historyArray.slice(); + const present = sliced[isRedo ? "shift" : "pop"](); + if (present) { + updateFromUndoable = true; + storeRefManager.partialUpdate(present); + return { + present, + future: isRedo ? sliced : [storeHistory.present, ...storeHistory.future], + past: isRedo ? [...storeHistory.past, storeHistory.present] : sliced, + }; + } + } + }; + + storeRefManager.subscribe((partialUpdate, prevState, currState) => { + if (updateFromUndoable) { + updateFromUndoable = false; + } else { + const pastLength = storeHistory.past.length; + + manage(storeHistory).partialUpdate({ + present: currState, + future: storeHistory.future.length ? [] : storeHistory.future, + past: [...storeHistory.past.slice(+(pastLength >= limit), pastLength), prevState], + }); + } + }); + + return { + undo: effect(storeHistory, () => undoRedo()), + redo: effect(storeHistory, () => undoRedo(true)), + history: storeHistory, + }; +};