Skip to content

Commit 5529feb

Browse files
Initial commit
0 parents  commit 5529feb

15 files changed

+464
-0
lines changed

.eslintrc.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = {
2+
"env": {
3+
"browser": true,
4+
"commonjs": true,
5+
"es2021": true,
6+
"node": true
7+
},
8+
"extends": "eslint:recommended",
9+
"parserOptions": {
10+
"ecmaVersion": 12
11+
},
12+
"rules": {
13+
}
14+
};

.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/coverage/
2+
/.nyc_output/
3+
/dist/
4+
/node_modules/
5+
/trash/
6+
/.idea/
7+
/test/tests/decorators.js
8+
/test/tests/decorators.legacy.js
9+
/playground/dist/

.npmignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!dist/**

.travis.yml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
sudo: false
2+
language: node_js
3+
node_js:
4+
- "14"
5+
env:
6+
- NODE_ENV=TEST
7+
script:
8+
- npm run test

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Change Log
2+
All notable changes to this project will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](http://keepachangelog.com/)
5+
and this project adheres to [Semantic Versioning](http://semver.org/).
6+
7+
## [0.1.0] - 2021-01-04
8+
9+
### Added
10+
- initial version

lib/use-async-effect.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const {useEffect} = require("react");
2+
const {CPromise, CanceledError} = require("c-promise2");
3+
4+
const {E_REASON_UNMOUNTED} = CanceledError.registerErrors({
5+
E_REASON_UNMOUNTED: 'unmounted'
6+
})
7+
8+
const isGeneratorFn = (thing) => typeof thing === 'function' &&
9+
thing.constructor &&
10+
thing.constructor.name === 'GeneratorFunction';
11+
12+
const useAsyncEffect = (generator, deps) => {
13+
let _promise;
14+
15+
useEffect(() => {
16+
if (!isGeneratorFn(generator)) {
17+
throw TypeError('useAsyncEffect requires a generator as the first argument');
18+
}
19+
20+
const promise = CPromise.resolveGenerator(generator, {resolveSignatures: true});
21+
_promise = promise;
22+
23+
let cb;
24+
25+
promise.then(_cb => {
26+
if (_cb !== undefined && typeof _cb !== 'function') {
27+
throw TypeError('useAsyncEffect handler should return a function');
28+
}
29+
cb = _cb;
30+
});
31+
32+
return () => {
33+
promise.cancel(E_REASON_UNMOUNTED);
34+
_promise = null;
35+
cb && cb();
36+
}
37+
}, deps);
38+
39+
return [
40+
(reason) => !!_promise && _promise.cancel(reason)
41+
]
42+
}
43+
44+
module.exports = {
45+
useAsyncEffect,
46+
E_REASON_UNMOUNTED
47+
}

package.json

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{
2+
"name": "use-async-effect",
3+
"version": "0.0.0",
4+
"description": "cancellable async effect hook",
5+
"main": "lib/index.js",
6+
"scripts": {
7+
"test": "npm run test:build && mocha-headless-chrome -f test/test.html",
8+
"test:build": "rollup ./test/src/index.js --file ./test/build/index.js --format iife --config ./test/rollup.config.js",
9+
"playground": "npm run playground:build && concurrently --kill-others \"npm run playground:server\" \"npm run playground:watch\"",
10+
"playground:run": "node playground/build/index.js || true",
11+
"playground:build": "rollup ./playground/src/index.js --file ./playground/build/index.js --format iife --config ./playground/rollup.config.js",
12+
"playground:watch": "nodemon --delay 1500ms --watch ./playground/src/ --watch lib/ --exec \\\"npm run playground:build\\\"",
13+
"playground:server": "browser-sync start --server --index ./playground/index.html --files \"./playground/build/*.*\""
14+
},
15+
"author": {
16+
"name": "Dmitriy Mozgovoy",
17+
"email": "[email protected]",
18+
"url": "http://github.com/DigitalBrainJS/"
19+
},
20+
"license": "MIT",
21+
"keywords": [
22+
"promise",
23+
"cancelable",
24+
"cancellable",
25+
"p-cancelable",
26+
"timeout",
27+
"progress",
28+
"cancel",
29+
"abortable",
30+
"abort",
31+
"AbortController",
32+
"AbortSignal",
33+
"async",
34+
"signal",
35+
"await",
36+
"promises",
37+
"generator",
38+
"co",
39+
"yield",
40+
"reject",
41+
"race",
42+
"decorator",
43+
"delay",
44+
"break",
45+
"suspending",
46+
"wait",
47+
"bluebird",
48+
"deferred",
49+
"react",
50+
"setState",
51+
"cancellation",
52+
"aborting",
53+
"close",
54+
"closable",
55+
"pause",
56+
"task"
57+
],
58+
"dependencies": {
59+
"browser-sync": "^2.26.13",
60+
"c-promise2": "^0.10.8",
61+
"react": "^17.0.1",
62+
"react-dom": "^17.0.1"
63+
},
64+
"devDependencies": {
65+
"@babel/core": "^7.12.10",
66+
"@babel/preset-react": "^7.12.10",
67+
"@rollup/plugin-babel": "^5.2.2",
68+
"@rollup/plugin-commonjs": "^17.0.0",
69+
"@rollup/plugin-json": "^4.1.0",
70+
"@rollup/plugin-node-resolve": "^11.0.1",
71+
"@rollup/plugin-replace": "^2.3.4",
72+
"assert": "^2.0.0",
73+
"chai": "^4.2.0",
74+
"concurrently": "^5.3.0",
75+
"cp-fetch": "^0.1.8",
76+
"eslint": "^7.17.0",
77+
"mocha": "^8.2.1",
78+
"mocha-headless-chrome": "^3.1.0",
79+
"nodemon": "^2.0.6",
80+
"prop-types": "^15.7.2",
81+
"react-is": "^17.0.1",
82+
"rollup": "^2.35.1"
83+
}
84+
}

playground/index.html

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Playground</title>
6+
</head>
7+
<body>
8+
<style>
9+
.field-url{
10+
width: 600px;
11+
}
12+
13+
.btn-change{
14+
margin-left: 5px;
15+
}
16+
17+
#app>div{
18+
margin: 10px 0px ;
19+
}
20+
21+
22+
</style>
23+
<div id="root"></div>
24+
<script src="/playground/build/index.js" async></script>
25+
</body>
26+
</html>

playground/rollup.config.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import resolve from '@rollup/plugin-node-resolve';
2+
import commonjs from '@rollup/plugin-commonjs';
3+
import babel from '@rollup/plugin-babel';
4+
import replace from '@rollup/plugin-replace';
5+
6+
export default
7+
{
8+
plugins: [
9+
babel({
10+
exclude: 'node_modules/**',
11+
babelHelpers: 'bundled',
12+
plugins: [],
13+
presets: [
14+
"@babel/preset-react"
15+
]
16+
}),
17+
replace({
18+
'process.env.NODE_ENV': JSON.stringify( 'production' )
19+
}),
20+
resolve({browser: true}),
21+
commonjs({
22+
extensions: ['.js', '.jsx']
23+
})
24+
]
25+
};

playground/src/App.js

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from "react";
2+
import TestComponent from "./TestComponent";
3+
4+
export default class App extends React.Component {
5+
constructor(props) {
6+
super(props);
7+
this.state = {url: props.url, _url: props.url};
8+
this.onClick = this.onClick.bind(this);
9+
this.onFetchClick = this.onFetchClick.bind(this);
10+
this.handleChange= this.handleChange.bind(this);
11+
}
12+
13+
onFetchClick(){
14+
console.log(`fetch click`);
15+
this.setState(({_url})=> this.setState({url: _url}))
16+
}
17+
18+
onClick() {
19+
console.log(`click`);
20+
this.setState({ timestamp: Date.now() });
21+
}
22+
23+
handleChange(event){
24+
console.log(event.target.value);
25+
this.setState({_url: event.target.value});
26+
}
27+
28+
render() {
29+
return (
30+
<div key={this.state.timestamp} id="app">
31+
<input className="field-url" type="text" value={this.state._url} onChange={this.handleChange} />
32+
<button className="btn-change" onClick={this.onFetchClick} disabled={this.state.url==this.state._url}>Change URL to Fetch</button>
33+
<TestComponent url={this.state.url}></TestComponent>
34+
<button onClick={this.onClick}>Remount</button>
35+
</div>
36+
);
37+
}
38+
}

playground/src/TestComponent.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from "react";
2+
import {useEffect, useState} from "react";
3+
import ReactDOM from "react-dom";
4+
import {useAsyncEffect, E_REASON_UNMOUNTED} from "../../lib/use-async-effect";
5+
import {CanceledError, CPromise} from "c-promise2";
6+
import cpFetch from "cp-fetch";
7+
8+
export default function TestComponent(props) {
9+
const [text, setText] = useState("");
10+
11+
const [cancel, ref]= useAsyncEffect(function* ({onCancel}) {
12+
console.log("mount");
13+
14+
//this.timeout(1000);
15+
16+
onCancel(()=> console.log('scope canceled'));
17+
18+
try {
19+
setText("fetching...");
20+
const response = yield cpFetch(props.url);
21+
const json = yield response.json();
22+
setText(`Success: ${JSON.stringify(json)}`);
23+
} catch (err) {
24+
CanceledError.rethrow(err, E_REASON_UNMOUNTED); //passthrough
25+
setText(`Failed: ${err}`);
26+
}
27+
28+
return () => {
29+
console.log("unmount", this.isCanceled);
30+
};
31+
}, [props.url]);
32+
33+
//setTimeout(()=> cancel("Ooops!"), 1000);
34+
35+
return <div>{text}</div>;
36+
}

playground/src/index.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React from "react";
2+
import ReactDOM from "react-dom";
3+
import App from "./App";
4+
5+
ReactDOM.render(
6+
<App url="https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s"></App>,
7+
document.getElementById('root')
8+
);

test/rollup.config.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import resolve from '@rollup/plugin-node-resolve';
2+
import commonjs from '@rollup/plugin-commonjs';
3+
import babel from '@rollup/plugin-babel';
4+
import replace from '@rollup/plugin-replace';
5+
import json from '@rollup/plugin-json';
6+
7+
export default
8+
{
9+
plugins: [
10+
babel({
11+
exclude: 'node_modules/**',
12+
babelHelpers: 'bundled',
13+
plugins: [],
14+
presets: [
15+
"@babel/preset-react"
16+
]
17+
}),
18+
replace({
19+
'process.env.NODE_ENV': JSON.stringify( 'production' )
20+
}),
21+
resolve({
22+
browser: true,
23+
preferBuiltins: false
24+
}),
25+
json(),
26+
commonjs({
27+
extensions: ['.js', '.jsx']
28+
})
29+
]
30+
};

0 commit comments

Comments
 (0)