Skip to content
This repository was archived by the owner on Jun 2, 2024. It is now read-only.

Commit 38c2f0a

Browse files
bergelfcsvenke
authored andcommitted
Render react components based on a promise (#4)
* Adds Resolve component to handle promises
1 parent d8c836d commit 38c2f0a

File tree

7 files changed

+240
-0
lines changed

7 files changed

+240
-0
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { mount } from 'enzyme';
2+
import * as React from 'react';
3+
import Resolve from './Resolve';
4+
5+
describe('resolve', () => {
6+
const timeout = 10;
7+
8+
const resolvable = resolved =>
9+
new Promise(resolve => {
10+
setTimeout(() => {
11+
resolve(resolved);
12+
}, timeout);
13+
});
14+
15+
const rejectable = rejected =>
16+
new Promise((_, reject) => {
17+
setTimeout(() => {
18+
reject(rejected);
19+
}, timeout);
20+
});
21+
22+
test('no pending, resolve, reject', () => {
23+
const wrapper = mount(<Resolve promise={resolvable('resolved')} />);
24+
expect(wrapper.html()).toEqual(null);
25+
});
26+
27+
test('pending', () => {
28+
const pending = <p>horse</p>;
29+
const result = '<p>horse</p>';
30+
const wrapper = mount(
31+
<Resolve promise={resolvable('resolved')} pending={pending} />,
32+
);
33+
expect(wrapper.html()).toEqual(result);
34+
});
35+
36+
test('resolve', done => {
37+
const resolved = value => <p>{value}</p>;
38+
const result = '<p>resolved</p>';
39+
const wrapper = mount(
40+
<Resolve promise={resolvable('resolved')} resolved={resolved} />,
41+
);
42+
setTimeout(() => {
43+
expect(wrapper.html()).toEqual(result);
44+
done();
45+
}, timeout + 5);
46+
});
47+
48+
test('resolve', done => {
49+
const rejected = value => <p>{value}</p>;
50+
const result = '<p>rejected</p>';
51+
const wrapper = mount(
52+
<Resolve promise={rejectable('rejected')} rejected={rejected} />,
53+
);
54+
setTimeout(() => {
55+
expect(wrapper.html()).toEqual(result);
56+
done();
57+
}, timeout + 5);
58+
});
59+
});

src/components/Resolve/Resolve.tsx

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import * as PropTypes from 'prop-types';
2+
import * as React from 'react';
3+
import { isPromise } from '../../utils';
4+
import { statusTypes } from './config';
5+
6+
export interface IResolveProps {
7+
/** The promise to be resolved */
8+
promise?: Promise<any>;
9+
10+
/** Will be displayed after promise is resolved */
11+
resolved?: (value) => React.ReactNode;
12+
13+
/** Will be displayed while promise is handled */
14+
pending?: React.ReactNode;
15+
16+
/** Will be displayed if promise is rejected */
17+
rejected?: (error) => React.ReactNode;
18+
}
19+
20+
const initialState = {
21+
status: statusTypes.none,
22+
value: '',
23+
};
24+
25+
type IResolveState = Readonly<typeof initialState>;
26+
27+
/**
28+
* A component to render based on a promise
29+
*
30+
* @example
31+
* <Resolve
32+
* promise = {aPromise}
33+
* resolved = {
34+
* value => <p>{`Resolved value is ${value}`}</p>
35+
* } />
36+
*/
37+
class Resolve extends React.Component<IResolveProps, IResolveState> {
38+
// PropTypes
39+
public static propTypes = {
40+
pending: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
41+
promise: isPromise,
42+
rejected: PropTypes.func,
43+
resolved: PropTypes.func,
44+
};
45+
46+
// Initialize state
47+
public readonly state: IResolveState = initialState;
48+
49+
// Create escape hatch to stop handling of promise if unmounted
50+
public unmounted = false;
51+
52+
public componentDidMount() {
53+
// Start handling the promise, must happen after mount as setState is called when promise is handled
54+
this._handlePromise(this.props.promise);
55+
}
56+
57+
public componentDidUpdate(prevProps) {
58+
if (this.props.promise !== prevProps.promise) {
59+
this.setState({
60+
status: statusTypes.none,
61+
});
62+
this._handlePromise(this.props.promise);
63+
}
64+
}
65+
66+
public componentWillUnmount() {
67+
this.unmounted = true;
68+
}
69+
70+
// Promise resolver function
71+
public _handlePromise(promise) {
72+
// Store the current promise to fast exit if promise is change during handling
73+
const currentPromise = promise;
74+
this.setState({
75+
status: statusTypes.pending,
76+
});
77+
promise
78+
.then(success => {
79+
// Escape early as promise is changed
80+
if (currentPromise !== promise) {
81+
return;
82+
}
83+
if (!this.unmounted) {
84+
this.setState({
85+
status: statusTypes.resolved,
86+
value: success,
87+
});
88+
}
89+
})
90+
.catch(reason => {
91+
// Escape early as promise is changed
92+
if (currentPromise !== promise) {
93+
return;
94+
}
95+
if (!this.unmounted) {
96+
this.setState({
97+
status: statusTypes.rejected,
98+
value: reason,
99+
});
100+
}
101+
});
102+
}
103+
104+
public render() {
105+
const { pending, resolved, rejected } = this.props;
106+
const { status, value } = this.state;
107+
108+
switch (status) {
109+
case statusTypes.none:
110+
break;
111+
case statusTypes.pending:
112+
if (pending) {
113+
return pending;
114+
}
115+
break;
116+
case statusTypes.resolved:
117+
if (resolved) {
118+
return resolved(value);
119+
}
120+
break;
121+
case statusTypes.rejected:
122+
if (rejected) {
123+
return rejected(value);
124+
}
125+
break;
126+
default:
127+
break;
128+
}
129+
130+
return null;
131+
}
132+
}
133+
134+
export default Resolve;

src/components/Resolve/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const statusTypes = {
2+
none: 'none',
3+
pending: 'pending',
4+
rejected: 'rejected',
5+
resolved: 'resolved',
6+
};

src/components/Resolve/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './Resolve';

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './isEmptyChildren';
2+
export * from './isPromise/isPromise';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { isPromise } from './isPromise';
2+
3+
describe('isPromise type check', () => {
4+
test('Should return positive if supplied a promise', () => {
5+
const prom = new Promise(resolve => {
6+
setTimeout(() => {
7+
resolve(1);
8+
}, 10);
9+
});
10+
const result = isPromise({ prom }, 'prom', 'component');
11+
expect(result).toEqual(null);
12+
});
13+
test('Should return positive if supplied a nothing', () => {
14+
const result = isPromise({}, 'prom', 'component');
15+
expect(result).toEqual(null);
16+
});
17+
[
18+
'not a promise',
19+
a => a,
20+
true,
21+
1,
22+
['contains', 'not', 'a', 'promise'],
23+
null,
24+
undefined,
25+
].forEach(notPromise =>
26+
test(`Should return negative if supplied a ${typeof notPromise}`, () => {
27+
const result = isPromise({ prom: 'not a promise' }, 'prom', 'component');
28+
expect(result).toMatchObject(new Error('prom in component is not a promise'));
29+
}),
30+
);
31+
});

src/utils/isPromise/isPromise.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const isPromise = (props: object, propName: string, componentName: string) => {
2+
if (props[propName]) {
3+
return props[propName] instanceof Promise
4+
? null
5+
: new Error(`${propName} in ${componentName} is not a promise`);
6+
}
7+
return null;
8+
};

0 commit comments

Comments
 (0)