Skip to content

Commit eb83466

Browse files
Refactored useDeepState hook;
Removed support of old state resolving by `useDeepState` hook; Added `once` option for `useAsyncEffect` hook;
1 parent 84008f3 commit eb83466

File tree

7 files changed

+172
-110
lines changed

7 files changed

+172
-110
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -339,11 +339,11 @@ export default function TestComponent(props) {
339339
<div className="caption">useAsyncDeepState demo:</div>
340340
<div>{state.counter}</div>
341341
<button onClick={async()=>{
342-
const [newState, oldState]= await setState((state)=> {
342+
const newState= await setState((state)=> {
343343
return {counter: state.counter + 1}
344344
});
345345

346-
console.log(`Updated: ${newState.counter}, old: ${oldState.counter}`);
346+
console.log(`Updated: ${newState.counter}`);
347347
}}>Inc</button>
348348
<button onClick={()=>setState({
349349
counter: state.counter

lib/use-async-effect.js

+109-79
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const {useEffect, useMemo, useRef, useState} = require("react");
22
const {CPromise, CanceledError, E_REASON_UNMOUNTED} = require("c-promise2");
3-
const isEqualObjects = require('is-equal-objects/dist/is-equal-objects.cjs');
3+
const {isEqualObjects, cloneObject} = require('is-equal-objects');
44

55
const {E_REASON_QUEUE_OVERFLOW, E_REASON_RESTART} = CanceledError.registerErrors({
66
E_REASON_QUEUE_OVERFLOW: 'overflow',
@@ -51,6 +51,7 @@ const removeElement = (arr, element) => {
5151
* @param {deps} [options.deps= []]
5252
* @param {boolean} [options.skipFirst= false]
5353
* @param {boolean} [options.states= false]
54+
* @param {boolean} [options.once= false]
5455
* @returns {UseAsyncEffectCancelFn}
5556
*/
5657
const useAsyncEffect = (generator, options) => {
@@ -59,16 +60,19 @@ const useAsyncEffect = (generator, options) => {
5960
let {
6061
deps = [],
6162
skipFirst= false,
62-
states = false
63+
states = false,
64+
once = false
6365
} = options && Array.isArray(options) ? {deps: options} : options || {};
6466

65-
const [state, setState]= states? useAsyncDeepState({
67+
const initialState = {
6668
done: false,
6769
result: undefined,
6870
error: undefined,
6971
canceled: false,
7072
paused: false
71-
}, {watch: false}) : [];
73+
};
74+
75+
const [state, setState] = states ? useAsyncDeepState(initialState, {watch: false}) : [];
7276

7377
if (!isGeneratorFn(generator)) {
7478
throw TypeError('useAsyncEffect requires a generator as the first argument');
@@ -100,8 +104,6 @@ const useAsyncEffect = (generator, options) => {
100104
return cancel;
101105
});
102106

103-
104-
105107
useEffect(() => {
106108
if (!current.inited) {
107109
current.inited = true;
@@ -110,10 +112,19 @@ const useAsyncEffect = (generator, options) => {
110112
}
111113
}
112114

115+
if (once && current.done) {
116+
return;
117+
}
118+
113119
let cb;
114120

121+
states && setState(initialState);
122+
115123
let promise = current.promise = CPromise.run(generator, {resolveSignatures: true, scopeArg: true})
116124
.then(result => {
125+
126+
current.done = true;
127+
117128
if (typeof result === 'function') {
118129
cb = result;
119130
states && setState({
@@ -464,112 +475,131 @@ const useFactory = (factory, args) => {
464475

465476
initialized.add(current);
466477

467-
Object.assign(current, factory.apply(null, args));
468-
469-
return current;
478+
return Object.assign(current, factory.apply(null, args));
470479
}
471480

472-
const assignState= (obj, target)=>{
473-
Object.assign(obj, target);
481+
const assignEnumerableProps = (source, target) => {
482+
Object.assign(source, target);
474483
const symbols = Object.getOwnPropertySymbols(target);
475484
let i = symbols.length;
476485
while (i-- > 0) {
477-
const symbol = symbols[i];
478-
obj[symbol] = target[symbol];
486+
let symbol = symbols[i];
487+
source[symbol] = target[symbol];
479488
}
480-
return obj;
489+
return source;
481490
}
482491

483-
class ProtoState {
484-
constructor(initialState) {
485-
initialState && assignState(this, initialState);
486-
}
492+
const protoState = Object.create(null, {
493+
toJSON: {
494+
value: function toJSON(){
495+
const obj = {};
496+
let target = this;
497+
do {
498+
assignEnumerableProps(obj, target);
499+
} while ((target = Object.getPrototypeOf(target)) && target !== Object.prototype);
500+
return obj;
501+
}
502+
},
487503

488-
toJSON() {
489-
const obj = {};
490-
let target = this;
491-
do {
492-
assignState(obj, target);
493-
} while ((target = Object.getPrototypeOf(target)) && target !== Object.prototype);
494-
return obj;
504+
[isEqualObjects.plainObject]: {
505+
value: true
495506
}
496-
}
507+
})
508+
509+
const getAllKeys = (obj) => Object.keys(obj).concat(Object.getOwnPropertyNames(obj));
497510

498511
/**
499512
* useAsyncDeepState hook whose setter returns a promise
500-
* @param {*} [initialValue]
513+
* @param {*} [initialState]
501514
* @param {Boolean} [watch= true]
515+
* @param {Boolean} [defineSetters= true]
502516
* @returns {[any, function(*=, boolean=): (Promise<*>|undefined)]}
503517
*/
504-
const useAsyncDeepState = (initialValue, {watch= true}= {}) => {
505-
const {current} = useRef({});
518+
const useAsyncDeepState = (initialState, {watch = true, defineSetters = true}= {}) => {
506519

507-
if(!current.inited){
508-
if (initialValue !== undefined && typeof initialValue !== "object") {
520+
const current = useFactory(()=>{
521+
if (initialState !== undefined && typeof initialState !== "object") {
509522
throw TypeError('initial state must be a plain object');
510523
}
511524

512-
const state = new ProtoState(initialValue);
525+
const setter = (patch, scope, cb)=>{
526+
setState((state)=>{
527+
if (typeof patch === 'function') {
528+
patch = patch(state);
529+
}
530+
531+
if (patch!==true && patch != null && typeof patch !== 'object') {
532+
throw new TypeError('patch must be a plain object or boolean');
533+
}
534+
535+
if (
536+
patch !== true &&
537+
(patch === null || assignEnumerableProps(current.state, patch)) &&
538+
(!current.stateChanged && isEqualObjects(current.state, current.snapshot))
539+
) {
540+
scope && cb(state);
541+
return state;
542+
}
543+
544+
current.stateChanged = true;
513545

514-
Object.assign(current, {
546+
if (scope) {
547+
current.callbacks.set(scope, cb);
548+
scope.onDone(() => current.callbacks.delete(scope))
549+
}
550+
551+
return Object.freeze(Object.create(current.proxy));
552+
});
553+
}
554+
555+
const state = assignEnumerableProps(Object.create(protoState), initialState)
556+
557+
const proxy = Object.create(state, defineSetters && getAllKeys(initialState)
558+
.reduce((props, prop) => {
559+
props[prop] = {
560+
get() {
561+
return state[prop];
562+
},
563+
set(value) {
564+
state[prop] = value;
565+
setter(null);
566+
}
567+
}
568+
return props;
569+
}, {}));
570+
571+
return {
515572
state,
516-
oldState: new ProtoState(initialValue),
517-
callbacks: new Map()
518-
})
519-
}
573+
snapshot: null,
574+
proxy,
575+
initialState: Object.freeze(Object.create(proxy)),
576+
stateChanged: false,
577+
callbacks: new Map(),
578+
setter
579+
}
580+
});
520581

521-
const [state, setState] = useState(!current.inited && Object.freeze(Object.create(current.state)));
582+
const [state, setState] = useState(current.initialState);
522583

523-
watch && useEffect(()=>{
524-
if (!current.inited) return;
525-
const data= [state, current.oldState];
526-
current.callbacks.forEach(cb=> cb(data));
584+
useEffect(()=>{
585+
current.stateChanged = false;
586+
current.callbacks.forEach(cb=> cb(state));
527587
current.callbacks.clear();
528-
current.oldState= new ProtoState(current.state);
588+
current.snapshot= cloneObject(current.state);
529589
}, [state]);
530590

531-
current.inited= true;
532-
533591
return [
534592
state,
535593
/**
536594
* state async accessor
537-
* @param {Object} [newState]
595+
* @param {Object} [handlerOrPatch]
596+
* @param {Boolean} [watchChanges= true]
538597
* @returns {Promise<*>|undefined}
539598
*/
540-
function (newState) {
541-
const setter= (scope, cb)=>{
542-
if (typeof newState === 'function') {
543-
newState = newState(current.state);
544-
}
545-
546-
if (newState != null && typeof newState !== 'object') {
547-
throw new TypeError('new state must be a plain object');
548-
}
549-
550-
setState((state)=>{
551-
if(newState == null || isEqualObjects(assignState(current.state, newState), current.oldState)){
552-
scope && cb([state, current.oldState]);
553-
return state;
554-
}
555-
556-
if (scope) {
557-
current.callbacks.set(scope, cb);
558-
scope.onDone(() => current.callbacks.delete(scope))
559-
}
560-
561-
return Object.freeze(Object.create(current.state));
562-
});
563-
}
564-
565-
if (!watch) {
566-
setter();
567-
return;
568-
}
569-
570-
return new CPromise((resolve, reject, scope) => {
571-
setter(scope, resolve);
572-
});
599+
function (handlerOrPatch, watchChanges= watch) {
600+
return watchChanges ? new CPromise((resolve, reject, scope) => {
601+
current.setter(handlerOrPatch, scope, resolve);
602+
}) : current.setter(handlerOrPatch);
573603
}
574604
]
575605
}

package-lock.json

+14-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@
8484
"commitLimit": false
8585
},
8686
"dependencies": {
87-
"c-promise2": "^0.13.5",
88-
"is-equal-objects": "^0.2.2"
87+
"c-promise2": "^0.13.7",
88+
"is-equal-objects": "^0.3.0"
8989
},
9090
"peerDependencies": {
9191
"react": ">=16.8.0"

playground/src/App.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import TestComponent6 from "./TestComponent6";
88
import TestComponent7 from "./TestComponent7";
99
import TestComponent8 from "./TestComponent8";
1010
import TestComponent9 from "./TestComponent9";
11+
import LiveTest from "./states";
1112

1213
export default class App extends React.Component {
1314
constructor(props) {
@@ -66,15 +67,16 @@ export default class App extends React.Component {
6667
Timeout [{this.state.timeout}]ms
6768
</label>
6869
</div>
69-
<TestComponent1 url={this.state.url} timeout={this.state.timeout}></TestComponent1>
70+
{/* <TestComponent1 url={this.state.url} timeout={this.state.timeout}></TestComponent1>
7071
<TestComponent2 url={this.state.url} timeout={this.state.timeout}></TestComponent2>
7172
<TestComponent3 url={this.state.url} timeout={this.state.timeout}></TestComponent3>
7273
<TestComponent4 url={this.state.url} timeout={this.state.timeout}></TestComponent4>
7374
<TestComponent5 url={this.state.url} timeout={this.state.timeout}></TestComponent5>
74-
<TestComponent6 url={this.state.url} timeout={this.state.timeout}></TestComponent6>
75+
<TestComponent6 url={this.state.url} timeout={this.state.timeout}></TestComponent6>*/}
7576
<TestComponent7 url={this.state.url} timeout={this.state.timeout}></TestComponent7>
76-
<TestComponent8 url={this.state.url} timeout={this.state.timeout}></TestComponent8>
77+
{/* <TestComponent8 url={this.state.url} timeout={this.state.timeout}></TestComponent8>
7778
<TestComponent9 url={this.state.url} timeout={this.state.timeout}></TestComponent9>
79+
<LiveTest></LiveTest>*/}
7880
<button className="btn btn-danger" onClick={this.onClick}>
7981
Remount component
8082
</button>

0 commit comments

Comments
 (0)