From a482ff37733a5c8b12eb563e616c7ea82fae1fc0 Mon Sep 17 00:00:00 2001 From: "zhanwen.zw" Date: Tue, 2 Jul 2019 17:57:36 +0800 Subject: [PATCH 01/11] refactor: bindings --- package.json | 3 +- src/store.ts | 90 +++++++++++++++++++++++++++++++++++++++------------- src/util.ts | 31 ++++++++++++++++++ 3 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 src/util.ts diff --git a/package.json b/package.json index f870d5cf..c3e99925 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "typescript": "^3.4.4" }, "dependencies": { - "lodash.isfunction": "^3.0.9" + "lodash.isfunction": "^3.0.9", + "lodash.isobject": "^3.0.2" }, "peerDependencies": { "react": "^16.7.0" diff --git a/src/store.ts b/src/store.ts index 03574eb1..5b606c6b 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,50 +1,97 @@ - import * as isFunction from 'lodash.isfunction'; import { useState, useEffect } from 'react'; +import { addProxy } from './util'; interface MethodFunc { (): void; } export default class Store { - private state: {[name: string]: any} = {}; - private methods: {[name: string]: MethodFunc} = {}; + // store state and actions user defined + private bindings: {[name: string]: any} = {}; + // queue of setState method from useState hook private queue = []; + // flag of whether allow state mutate + private allowMutate = false; + + // flag of whether state changed after mutation + private stateChanged = false; + public constructor(bindings: object) { Object.keys(bindings).forEach((key) => { const value = bindings[key]; - - if (isFunction(value)) { - this.methods[key] = this.createMethod(value); - } else { - this.state[key] = value; - } + this.bindings[key] = isFunction(value) ? this.createAction(value) : value; }); - } - private getBindings() { - return { ...this.state, ...this.methods }; + const handler = { + set: (target, prop, value) => { + if (!this.allowMutate) { + console.error('Forbid modifying state directly, use action to modify instead.'); + } + if (target[prop] !== value) { + this.stateChanged = true; + } + target[prop] = addProxy(value, handler); + return true; + }, + }; + + this.bindings = addProxy(this.bindings, handler); } - private createMethod(fun): MethodFunc { + /** + * Create action which will trigger state update after mutation + * @param {func} fun - original method user defined + * @return {func} action function + */ + private createAction(fun): MethodFunc { return async (...args) => { - const newState = { ...this.state }; - await fun.apply(newState, args); - this.setState(newState); - return this.getBindings(); + this.allowMutate = true; + await fun.apply(this.bindings, args); + if (this.stateChanged) { + this.setState(); + } + this.allowMutate = false; + this.stateChanged = false; + return this.bindings; }; } - private setState(newState: object): void { - this.state = newState; + /** + * Get state from bindings + * @return {object} state + */ + private getState(): object { + const { bindings } = this; + const state = {}; + Object.keys(bindings).forEach((key) => { + const value = bindings[key]; + if (!isFunction(value)) { + state[key] = value; + } + }); + return state; + } + + /** + * Trigger setState method in queue + */ + private setState(): void { + const state = this.getState(); + const newState = { ...state }; this.queue.forEach(setState => setState(newState)); } + /** + * Hook used to register setState and expose bindings + * @return {object} bindings of store + */ public useStore(): object { - const [, setState] = useState(this.state); + const state = this.getState(); + const [, setState] = useState(state); useEffect(() => { this.queue.push(setState); return () => { @@ -52,7 +99,6 @@ export default class Store { this.queue.splice(index, 1); }; }, []); - - return this.getBindings(); + return this.bindings; } } diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 00000000..a13285d0 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,31 @@ +import * as isObject from 'lodash.isobject'; +import * as isFunction from 'lodash.isfunction'; + +const isArray = Array.isArray; + +/** + * Recursively add proxy to javascipt variable + * @param {*} value - javascript variable of any type + * @param {func} handler - proxy handler + * @return {object} new proxy object + */ +export const addProxy = (value, handler) => { + if (!isObject(value) || isFunction(value)) { + return value; + } + + if (isArray(value)) { + value.forEach((item, index) => { + if (isObject(item)) { + value[index] = addProxy(item, handler); + } + }); + } else if (isObject(value)) { + Object.keys(value).forEach((key) => { + if (isObject(value[key])) { + value[key] = addProxy(value[key], handler); + } + }); + } + return new Proxy(value, handler); +}; From d8694beff557f303a59e8187dbfe454bd6f1de9e Mon Sep 17 00:00:00 2001 From: "zhanwen.zw" Date: Tue, 2 Jul 2019 17:58:38 +0800 Subject: [PATCH 02/11] chore: version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c3e99925..0f6f7071 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ice/store", - "version": "0.1.4", + "version": "0.2.0", "description": "基于 React Hooks 的数据流方案", "main": "lib/index.js", "types": "lib/index.d.ts", From a8bbd0a5a56cad1affc0415797cb095bfd9fa5b7 Mon Sep 17 00:00:00 2001 From: "zhanwen.zw" Date: Wed, 3 Jul 2019 10:33:27 +0800 Subject: [PATCH 03/11] fix: return when mutate flag is false --- src/store.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/store.ts b/src/store.ts index 5b606c6b..0282f29c 100644 --- a/src/store.ts +++ b/src/store.ts @@ -30,6 +30,7 @@ export default class Store { set: (target, prop, value) => { if (!this.allowMutate) { console.error('Forbid modifying state directly, use action to modify instead.'); + return true; } if (target[prop] !== value) { this.stateChanged = true; From d64143e13f4c8ffa5c5be122ba6489cfc51f42fb Mon Sep 17 00:00:00 2001 From: "zhanwen.zw" Date: Wed, 3 Jul 2019 10:57:57 +0800 Subject: [PATCH 04/11] fix: lint --- src/store.ts | 3 ++- src/util.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/store.ts b/src/store.ts index 0282f29c..e510d961 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,3 +1,5 @@ +/* eslint no-param-reassign: 0 */ + import * as isFunction from 'lodash.isfunction'; import { useState, useEffect } from 'react'; import { addProxy } from './util'; @@ -7,7 +9,6 @@ interface MethodFunc { } export default class Store { - // store state and actions user defined private bindings: {[name: string]: any} = {}; diff --git a/src/util.ts b/src/util.ts index a13285d0..b428b339 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,7 +1,9 @@ +/* eslint no-param-reassign: 0, import/prefer-default-export: 0 */ + import * as isObject from 'lodash.isobject'; import * as isFunction from 'lodash.isfunction'; -const isArray = Array.isArray; +const { isArray } = Array; /** * Recursively add proxy to javascipt variable From 88985f31001eb85c93d953f7deae2056521475c3 Mon Sep 17 00:00:00 2001 From: "zhanwen.zw" Date: Wed, 3 Jul 2019 11:17:20 +0800 Subject: [PATCH 05/11] chore: version adjust --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f6f7071..c2418736 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ice/store", - "version": "0.2.0", + "version": "0.1.5", "description": "基于 React Hooks 的数据流方案", "main": "lib/index.js", "types": "lib/index.d.ts", From c7aca7fef4a837daeac69c857e80a47758c64dc8 Mon Sep 17 00:00:00 2001 From: "zhanwen.zw" Date: Wed, 3 Jul 2019 11:30:57 +0800 Subject: [PATCH 06/11] fix: eslint lint rule --- src/store.ts | 3 +-- src/util.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/store.ts b/src/store.ts index e510d961..28e94752 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,5 +1,3 @@ -/* eslint no-param-reassign: 0 */ - import * as isFunction from 'lodash.isfunction'; import { useState, useEffect } from 'react'; import { addProxy } from './util'; @@ -36,6 +34,7 @@ export default class Store { if (target[prop] !== value) { this.stateChanged = true; } + /* eslint no-param-reassign: 0 */ target[prop] = addProxy(value, handler); return true; }, diff --git a/src/util.ts b/src/util.ts index b428b339..843d28e2 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,3 @@ -/* eslint no-param-reassign: 0, import/prefer-default-export: 0 */ - import * as isObject from 'lodash.isobject'; import * as isFunction from 'lodash.isfunction'; @@ -11,6 +9,7 @@ const { isArray } = Array; * @param {func} handler - proxy handler * @return {object} new proxy object */ +/* eslint no-param-reassign: 0, import/prefer-default-export: 0 */ export const addProxy = (value, handler) => { if (!isObject(value) || isFunction(value)) { return value; From c5893437d58d317b46aeefdbdbe361e4f9314d23 Mon Sep 17 00:00:00 2001 From: "zhanwen.zw" Date: Wed, 3 Jul 2019 17:37:47 +0800 Subject: [PATCH 07/11] feat: add util test --- src/util.ts | 19 +++++----- tests/util.spec.tsx | 85 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 tests/util.spec.tsx diff --git a/src/util.ts b/src/util.ts index 843d28e2..8bf68db3 100644 --- a/src/util.ts +++ b/src/util.ts @@ -4,29 +4,30 @@ import * as isFunction from 'lodash.isfunction'; const { isArray } = Array; /** - * Recursively add proxy to javascipt variable - * @param {*} value - javascript variable of any type - * @param {func} handler - proxy handler + * Recursively add proxy to object and array + * @param {*} value - variable of any type + * @param {object} handler - proxy handler * @return {object} new proxy object */ /* eslint no-param-reassign: 0, import/prefer-default-export: 0 */ -export const addProxy = (value, handler) => { +export function addProxy(value: any, handler: object): any { + // basic type (number, boolean, string, null, undefined) and function type if (!isObject(value) || isFunction(value)) { return value; } - if (isArray(value)) { + if (isArray(value)) { // array type value.forEach((item, index) => { - if (isObject(item)) { + if (isObject(item)) { // object and array type value[index] = addProxy(item, handler); } }); - } else if (isObject(value)) { + } else if (isObject(value)) { // object type Object.keys(value).forEach((key) => { - if (isObject(value[key])) { + if (isObject(value[key])) { // object and array type value[key] = addProxy(value[key], handler); } }); } return new Proxy(value, handler); -}; +} diff --git a/tests/util.spec.tsx b/tests/util.spec.tsx new file mode 100644 index 00000000..563dee73 --- /dev/null +++ b/tests/util.spec.tsx @@ -0,0 +1,85 @@ +import { addProxy } from '../src/util'; + +describe('#util', () => { + let handler; + + beforeEach(() => { + handler = { + set: (target, prop): boolean => { + /* eslint no-param-reassign: 0 */ + target[prop] = 'foo'; + return true; + }, + }; + }); + + describe('#addProxy', () => { + test('should boolean type not affected', () => { + const value = false; + expect(addProxy(value, handler)).toBe(value); + }); + + test('should number type not affected', () => { + const value = 1; + expect(addProxy(value, handler)).toBe(value); + }); + + test('should string type not affected', () => { + const value = 'abc'; + expect(addProxy(value, handler)).toBe(value); + }); + + test('should null type not affected', () => { + const value = null; + expect(addProxy(value, handler)).toBe(value); + }); + + test('should undefined type not affected', () => { + const value = undefined; + expect(addProxy(value, handler)).toBe(value); + }); + + test('should function type not affected', () => { + const value = () => {}; + expect(addProxy(value, handler)).toBe(value); + }); + + test('should object proxy set success', () => { + const value = { + a: 1, + b: 2, + }; + const result = addProxy(value, handler); + result.a = 100; + expect(result.a).toBe('foo'); + }); + + test('should object proxy recursively set success', () => { + const value = { + a: [ + { c: 3 }, + ], + b: 2, + }; + const result = addProxy(value, handler); + result.a[0].c = 4; + expect(result.a[0].c).toBe('foo'); + }); + + test('should array proxy set success', () => { + const value = [1, 2]; + const result = addProxy(value, handler); + result[0] = 4; + expect(result[0]).toBe('foo'); + }); + + test('should array proxy recursively set success', () => { + const value = [ + { a: 1 }, + ]; + const result = addProxy(value, handler); + result[0].a = 4; + expect(result[0].a).toBe('foo'); + }); + }); +}); From 5f8a3b6be8dc4e91361f274024850a87b833fb42 Mon Sep 17 00:00:00 2001 From: "zhanwen.zw" Date: Wed, 3 Jul 2019 17:56:00 +0800 Subject: [PATCH 08/11] chore: comment opt --- src/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index 8bf68db3..1ba4b39f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -11,7 +11,7 @@ const { isArray } = Array; */ /* eslint no-param-reassign: 0, import/prefer-default-export: 0 */ export function addProxy(value: any, handler: object): any { - // basic type (number, boolean, string, null, undefined) and function type + // primitive type (number, boolean, string, null, undefined) and function type if (!isObject(value) || isFunction(value)) { return value; } From 8e0a852c35999e3e9b110b4e50e7e107fbdbc9c9 Mon Sep 17 00:00:00 2001 From: "zhanwen.zw" Date: Wed, 3 Jul 2019 18:00:52 +0800 Subject: [PATCH 09/11] feat: restrict addProxy param to be object --- src/util.ts | 6 +++--- tests/util.spec.tsx | 25 ------------------------- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/src/util.ts b/src/util.ts index 1ba4b39f..02ec0a12 100644 --- a/src/util.ts +++ b/src/util.ts @@ -10,9 +10,9 @@ const { isArray } = Array; * @return {object} new proxy object */ /* eslint no-param-reassign: 0, import/prefer-default-export: 0 */ -export function addProxy(value: any, handler: object): any { - // primitive type (number, boolean, string, null, undefined) and function type - if (!isObject(value) || isFunction(value)) { +export function addProxy(value: object, handler: object): any { + // null and function also belongs to object type + if (value === null || isFunction(value)) { return value; } diff --git a/tests/util.spec.tsx b/tests/util.spec.tsx index 563dee73..972cfca3 100644 --- a/tests/util.spec.tsx +++ b/tests/util.spec.tsx @@ -14,31 +14,6 @@ describe('#util', () => { }); describe('#addProxy', () => { - test('should boolean type not affected', () => { - const value = false; - expect(addProxy(value, handler)).toBe(value); - }); - - test('should number type not affected', () => { - const value = 1; - expect(addProxy(value, handler)).toBe(value); - }); - - test('should string type not affected', () => { - const value = 'abc'; - expect(addProxy(value, handler)).toBe(value); - }); - - test('should null type not affected', () => { - const value = null; - expect(addProxy(value, handler)).toBe(value); - }); - - test('should undefined type not affected', () => { - const value = undefined; - expect(addProxy(value, handler)).toBe(value); - }); - test('should function type not affected', () => { const value = () => {}; expect(addProxy(value, handler)).toBe(value); From db7d2dd01f115da5302db78088176de5b1877321 Mon Sep 17 00:00:00 2001 From: "zhanwen.zw" Date: Wed, 3 Jul 2019 18:04:41 +0800 Subject: [PATCH 10/11] feat: throw error when modify state outside actions --- src/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store.ts b/src/store.ts index 28e94752..f897fb64 100644 --- a/src/store.ts +++ b/src/store.ts @@ -29,7 +29,7 @@ export default class Store { set: (target, prop, value) => { if (!this.allowMutate) { console.error('Forbid modifying state directly, use action to modify instead.'); - return true; + return false; } if (target[prop] !== value) { this.stateChanged = true; From a5b52404cfe8a8e10ab36858244cd39b3c743fce Mon Sep 17 00:00:00 2001 From: "zhanwen.zw" Date: Wed, 3 Jul 2019 18:07:37 +0800 Subject: [PATCH 11/11] feat: roll back primitive type logic in addProxy --- src/util.ts | 6 +++--- tests/util.spec.tsx | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/util.ts b/src/util.ts index 02ec0a12..1ba4b39f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -10,9 +10,9 @@ const { isArray } = Array; * @return {object} new proxy object */ /* eslint no-param-reassign: 0, import/prefer-default-export: 0 */ -export function addProxy(value: object, handler: object): any { - // null and function also belongs to object type - if (value === null || isFunction(value)) { +export function addProxy(value: any, handler: object): any { + // primitive type (number, boolean, string, null, undefined) and function type + if (!isObject(value) || isFunction(value)) { return value; } diff --git a/tests/util.spec.tsx b/tests/util.spec.tsx index 972cfca3..563dee73 100644 --- a/tests/util.spec.tsx +++ b/tests/util.spec.tsx @@ -14,6 +14,31 @@ describe('#util', () => { }); describe('#addProxy', () => { + test('should boolean type not affected', () => { + const value = false; + expect(addProxy(value, handler)).toBe(value); + }); + + test('should number type not affected', () => { + const value = 1; + expect(addProxy(value, handler)).toBe(value); + }); + + test('should string type not affected', () => { + const value = 'abc'; + expect(addProxy(value, handler)).toBe(value); + }); + + test('should null type not affected', () => { + const value = null; + expect(addProxy(value, handler)).toBe(value); + }); + + test('should undefined type not affected', () => { + const value = undefined; + expect(addProxy(value, handler)).toBe(value); + }); + test('should function type not affected', () => { const value = () => {}; expect(addProxy(value, handler)).toBe(value);