Skip to content

Commit 20c2802

Browse files
committedJun 30, 2021
Refactored & optimized useAsyncCallback hook;
1 parent 2cb3ca5 commit 20c2802

File tree

6 files changed

+167
-143
lines changed

6 files changed

+167
-143
lines changed
 

Diff for: ‎.npmignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
*
2-
!dist/**
2+
!lib/**
3+
!use-async-effect.d.ts

Diff for: ‎README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
## useAsyncEffect2 :snowflake:
88

9-
This library provides asynchronous versions of the `useEffect` and` useCallback` React hooks that can cancel
10-
asynchronous code inside it due to timeouts, user requests, or automatically when a component is unmounted or some
11-
dependency has changed.
9+
This library makes it possible to use cancellable async effects inside React components by providing asynchronous
10+
versions of the `useEffect` and` useCallback` hooks. These hooks can cancel asynchronous code inside it due to timeouts,
11+
user requests, or automatically when a component is unmounted or some dependency has changed.
1212

1313
The library is designed to make it as easy as possible to use complex and composite asynchronous routines
1414
in React components. It works on top of a [custom cancellable promise](https://www.npmjs.com/package/c-promise2),

Diff for: ‎lib/use-async-effect.js

+132-94
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,64 @@ const useAsyncEffect = (generator, options) => {
182182

183183
const argsToPromiseMap = new Map();
184184

185+
const asyncEffectFactory= (options) => {
186+
if (options != null) {
187+
if (typeof options !== 'object') {
188+
throw TypeError('options must be an object');
189+
}
190+
191+
if (options.threads === undefined) {
192+
options.threads = options.cancelPrevious || options.states ? 1 : 0;
193+
} else {
194+
if (!Number.isFinite(options.threads) || options.threads < 0) {
195+
throw Error('threads must be a positive number');
196+
}
197+
198+
if (options.states && options.threads !== 1) {
199+
throw Error(`Can not use states in not single threaded async function`);
200+
}
201+
}
202+
203+
if (options.queueSize !== undefined && (!Number.isFinite(options.queueSize) || options.queueSize < -1)) {
204+
throw Error('queueSize must be a finite number >=-1');
205+
}
206+
}
207+
208+
const promises= [];
209+
210+
return {
211+
promises,
212+
queue: [],
213+
pending: 0,
214+
args: null,
215+
cancel : (reason) => {
216+
const _reason = isEvent(reason) ? undefined : reason;
217+
promises.forEach(promise => promise.cancel(_reason));
218+
promises.length = 0;
219+
},
220+
pause: (data)=> promises.forEach(promise=> promise.pause(data)),
221+
resume: (data)=> promises.forEach(promise=> promise.resume(data)),
222+
initialState: {
223+
done: false,
224+
result: undefined,
225+
error: undefined,
226+
canceled: false,
227+
pending: false
228+
},
229+
options: {
230+
deps: [],
231+
combine: false,
232+
cancelPrevious: false,
233+
threads: 0,
234+
queueSize: -1,
235+
scopeArg: false,
236+
states: false,
237+
catchErrors: false,
238+
...(Array.isArray(options) ? {deps: options} : options)
239+
}
240+
};
241+
}
242+
185243
/**
186244
* @typedef {Function} UseAsyncCallbackDecoratedFn
187245
* @param {CPromise} [scope]
@@ -214,65 +272,32 @@ const argsToPromiseMap = new Map();
214272
* @returns {UseAsyncCallbackDecoratedFn}
215273
*/
216274
const useAsyncCallback = (generator, options) => {
217-
if (options != null && typeof options !== 'object') {
218-
throw TypeError('options must be an object or array');
219-
}
220-
221-
const initialState = {
222-
done: false,
223-
result: undefined,
224-
error: undefined,
225-
canceled: false,
226-
pending: false
227-
};
228-
229-
const {current} = useRef({
230-
promises: [],
231-
queue: [],
232-
pending: 0,
233-
args: null,
234-
callback: null
235-
});
275+
const current = useFactory(asyncEffectFactory, [options]);
236276

237277
let {
238-
deps = [],
239-
combine = false,
240-
cancelPrevious = false,
241-
threads,
242-
queueSize = -1,
243-
scopeArg = false,
244-
states = false,
245-
catchErrors = false
246-
} = options && Array.isArray(options) ? {deps: options} : options || {};
247-
248-
if (threads === undefined) {
249-
threads = cancelPrevious || states ? 1 : 0;
250-
} else {
251-
if (!Number.isFinite(threads) || threads < 0) {
252-
throw Error('threads must be a positive number');
278+
initialState,
279+
options: {
280+
deps,
281+
combine,
282+
cancelPrevious,
283+
threads,
284+
queueSize,
285+
scopeArg,
286+
states,
287+
catchErrors
253288
}
254-
}
255-
256-
if (queueSize !== -1 && (!Number.isFinite(queueSize) || queueSize < -1)) {
257-
throw Error('queueSize must be a finite number >=-1');
258-
}
289+
} = current;
259290

260291
const [state, setState] = states ? useAsyncDeepState(initialState, {watch: false}) : [];
261292

262-
const singleThreaded = threads === 1;
263-
264293
const callback = useMemo(() => {
265-
const {promises, queue}= current;
266-
267-
const cancel = (reason) => {
268-
const _reason = isEvent(reason) ? undefined : reason;
269-
promises.forEach(promise => promise.cancel(_reason));
270-
promises.length = 0;
271-
};
272-
273-
const pause= (data)=> promises.forEach(promise=> promise.pause(data));
274-
275-
const resume= (data)=> promises.forEach(promise=> promise.resume(data));
294+
const {
295+
promises,
296+
queue,
297+
cancel,
298+
pause,
299+
resume
300+
} = current;
276301

277302
const fn = (...args) => {
278303
let n;
@@ -304,7 +329,7 @@ const useAsyncCallback = (generator, options) => {
304329

305330
started = true;
306331

307-
if (states && singleThreaded) {
332+
if (states) {
308333
setState({
309334
...initialState,
310335
pending: true,
@@ -335,7 +360,7 @@ const useAsyncCallback = (generator, options) => {
335360
return;
336361
}
337362

338-
states && singleThreaded && setState({
363+
states && setState({
339364
pending: false,
340365
done: true,
341366
error: isRejected ? value : undefined,
@@ -346,7 +371,7 @@ const useAsyncCallback = (generator, options) => {
346371
return isRejected ? undefined : value;
347372
}).weight(0).aggregate();
348373

349-
if(states && singleThreaded){
374+
if(states){
350375
promise.onPause(()=> setState({
351376
paused: true
352377
}))
@@ -379,59 +404,72 @@ const useAsyncCallback = (generator, options) => {
379404
return promise;
380405
}
381406

407+
if(states) {
408+
const makeDescriptor = (name) => ({
409+
get() {
410+
return state[name];
411+
}
412+
})
413+
414+
Object.defineProperties(fn, {
415+
done: makeDescriptor('done'),
416+
pending: makeDescriptor('pending'),
417+
result: makeDescriptor('result'),
418+
error: makeDescriptor('error'),
419+
canceled: makeDescriptor('canceled'),
420+
paused: makeDescriptor('paused'),
421+
[Symbol.iterator]: {
422+
value: function*() {
423+
yield* [
424+
fn,
425+
cancel,
426+
state.pending,
427+
state.done,
428+
state.result,
429+
state.error,
430+
state.canceled,
431+
state.paused
432+
];
433+
}
434+
}
435+
})
436+
} else{
437+
fn[Symbol.iterator] = function*() {
438+
yield* [fn, cancel];
439+
}
440+
}
441+
382442
fn.cancel = cancel;
383443

384444
fn.pause= pause;
385445

386446
fn.resume= resume;
387447

388-
current.callback= fn;
389-
390-
Object.defineProperty(fn, 'current', {
391-
get(){
392-
return current.callback;
393-
},
394-
configurable: true
395-
});
396-
397448
return fn;
398449
}, deps);
399450

400451
useEffect(() => {
401452
return () => callback.cancel(E_REASON_UNMOUNTED);
402-
}, deps);
403-
404-
if(states) {
405-
if(!singleThreaded){
406-
throw Error(`Can not use states in not single threaded async function`);
407-
}
408-
callback.done = state.done;
409-
callback.pending = state.pending;
410-
callback.result = state.result;
411-
callback.error = state.error;
412-
callback.canceled = state.canceled;
413-
callback.paused = state.paused;
414-
415-
callback[Symbol.iterator] = function*() {
416-
yield* [
417-
callback,
418-
callback.cancel,
419-
state.pending,
420-
state.done,
421-
state.result,
422-
state.error,
423-
state.canceled,
424-
state.paused
425-
];
426-
}
427-
} else{
428-
callback[Symbol.iterator] = function*() {
429-
yield* [callback, callback.cancel];
430-
}
431-
}
453+
}, []);
432454

433455
return callback;
434456
}
457+
458+
const initialized= new WeakSet();
459+
460+
const useFactory = (factory, args) => {
461+
const {current} = useRef({});
462+
463+
if (initialized.has(current)) return current;
464+
465+
initialized.add(current);
466+
467+
Object.assign(current, factory.apply(null, args));
468+
469+
return current;
470+
}
471+
472+
435473
/**
436474
* useAsyncDeepState hook whose setter returns a promise
437475
* @param {*} [initialValue]

Diff for: ‎package.json

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "use-async-effect2",
33
"version": "0.11.2",
4-
"description": "Asynchronous versions of the `useEffect` and` useCallback` hooks that able to cancel internal code by user requests or component unmounting",
4+
"description": "Asynchronous & promisified versions of the `useEffect`, `useCallback` and `useState` hooks that able to cancel internal code on user requests or component unmounting",
55
"main": "lib/use-async-effect.js",
66
"scripts": {
77
"test": "npm run test:build && mocha-headless-chrome -f test/test.html",
@@ -23,10 +23,16 @@
2323
},
2424
"license": "MIT",
2525
"keywords": [
26-
"React",
26+
"react",
27+
"reactjs",
2728
"hook",
29+
"hooks",
2830
"useEffect",
2931
"useCallback",
32+
"useAsyncEffect",
33+
"use-async-effect",
34+
"async",
35+
"deepstate",
3036
"setState",
3137
"promise",
3238
"c-promise",
@@ -41,9 +47,9 @@
4147
"abort",
4248
"AbortController",
4349
"AbortSignal",
44-
"async",
4550
"signal",
4651
"await",
52+
"wait",
4753
"promises",
4854
"generator",
4955
"co",
@@ -54,7 +60,6 @@
5460
"delay",
5561
"break",
5662
"suspending",
57-
"wait",
5863
"bluebird",
5964
"deferred",
6065
"react",
@@ -63,8 +68,7 @@
6368
"close",
6469
"closable",
6570
"pause",
66-
"task",
67-
"reactjs"
71+
"task"
6872
],
6973
"repository": "https://github.com/DigitalBrainJS/use-async-effect.git",
7074
"bugs": {

0 commit comments

Comments
 (0)
Please sign in to comment.