diff --git a/src/components/Resolve/Resolve.test.tsx b/src/components/Resolve/Resolve.test.tsx
new file mode 100644
index 00000000..4a331f30
--- /dev/null
+++ b/src/components/Resolve/Resolve.test.tsx
@@ -0,0 +1,59 @@
+import { mount } from 'enzyme';
+import * as React from 'react';
+import Resolve from './Resolve';
+
+describe('resolve', () => {
+ const timeout = 10;
+
+ const resolvable = resolved =>
+ new Promise(resolve => {
+ setTimeout(() => {
+ resolve(resolved);
+ }, timeout);
+ });
+
+ const rejectable = rejected =>
+ new Promise((_, reject) => {
+ setTimeout(() => {
+ reject(rejected);
+ }, timeout);
+ });
+
+ test('no pending, resolve, reject', () => {
+ const wrapper = mount();
+ expect(wrapper.html()).toEqual(null);
+ });
+
+ test('pending', () => {
+ const pending =
horse
;
+ const result = 'horse
';
+ const wrapper = mount(
+ ,
+ );
+ expect(wrapper.html()).toEqual(result);
+ });
+
+ test('resolve', done => {
+ const resolved = value => {value}
;
+ const result = 'resolved
';
+ const wrapper = mount(
+ ,
+ );
+ setTimeout(() => {
+ expect(wrapper.html()).toEqual(result);
+ done();
+ }, timeout + 5);
+ });
+
+ test('resolve', done => {
+ const rejected = value => {value}
;
+ const result = 'rejected
';
+ const wrapper = mount(
+ ,
+ );
+ setTimeout(() => {
+ expect(wrapper.html()).toEqual(result);
+ done();
+ }, timeout + 5);
+ });
+});
diff --git a/src/components/Resolve/Resolve.tsx b/src/components/Resolve/Resolve.tsx
new file mode 100644
index 00000000..f3248c31
--- /dev/null
+++ b/src/components/Resolve/Resolve.tsx
@@ -0,0 +1,134 @@
+import * as PropTypes from 'prop-types';
+import * as React from 'react';
+import { isPromise } from '../../utils';
+import { statusTypes } from './config';
+
+export interface IResolveProps {
+ /** The promise to be resolved */
+ promise?: Promise;
+
+ /** Will be displayed after promise is resolved */
+ resolved?: (value) => React.ReactNode;
+
+ /** Will be displayed while promise is handled */
+ pending?: React.ReactNode;
+
+ /** Will be displayed if promise is rejected */
+ rejected?: (error) => React.ReactNode;
+}
+
+const initialState = {
+ status: statusTypes.none,
+ value: '',
+};
+
+type IResolveState = Readonly;
+
+/**
+ * A component to render based on a promise
+ *
+ * @example
+ * {`Resolved value is ${value}`}
+ * } />
+ */
+class Resolve extends React.Component {
+ // PropTypes
+ public static propTypes = {
+ pending: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
+ promise: isPromise,
+ rejected: PropTypes.func,
+ resolved: PropTypes.func,
+ };
+
+ // Initialize state
+ public readonly state: IResolveState = initialState;
+
+ // Create escape hatch to stop handling of promise if unmounted
+ public unmounted = false;
+
+ public componentDidMount() {
+ // Start handling the promise, must happen after mount as setState is called when promise is handled
+ this._handlePromise(this.props.promise);
+ }
+
+ public componentDidUpdate(prevProps) {
+ if (this.props.promise !== prevProps.promise) {
+ this.setState({
+ status: statusTypes.none,
+ });
+ this._handlePromise(this.props.promise);
+ }
+ }
+
+ public componentWillUnmount() {
+ this.unmounted = true;
+ }
+
+ // Promise resolver function
+ public _handlePromise(promise) {
+ // Store the current promise to fast exit if promise is change during handling
+ const currentPromise = promise;
+ this.setState({
+ status: statusTypes.pending,
+ });
+ promise
+ .then(success => {
+ // Escape early as promise is changed
+ if (currentPromise !== promise) {
+ return;
+ }
+ if (!this.unmounted) {
+ this.setState({
+ status: statusTypes.resolved,
+ value: success,
+ });
+ }
+ })
+ .catch(reason => {
+ // Escape early as promise is changed
+ if (currentPromise !== promise) {
+ return;
+ }
+ if (!this.unmounted) {
+ this.setState({
+ status: statusTypes.rejected,
+ value: reason,
+ });
+ }
+ });
+ }
+
+ public render() {
+ const { pending, resolved, rejected } = this.props;
+ const { status, value } = this.state;
+
+ switch (status) {
+ case statusTypes.none:
+ break;
+ case statusTypes.pending:
+ if (pending) {
+ return pending;
+ }
+ break;
+ case statusTypes.resolved:
+ if (resolved) {
+ return resolved(value);
+ }
+ break;
+ case statusTypes.rejected:
+ if (rejected) {
+ return rejected(value);
+ }
+ break;
+ default:
+ break;
+ }
+
+ return null;
+ }
+}
+
+export default Resolve;
diff --git a/src/components/Resolve/config.ts b/src/components/Resolve/config.ts
new file mode 100644
index 00000000..3e2dc1db
--- /dev/null
+++ b/src/components/Resolve/config.ts
@@ -0,0 +1,6 @@
+export const statusTypes = {
+ none: 'none',
+ pending: 'pending',
+ rejected: 'rejected',
+ resolved: 'resolved',
+};
diff --git a/src/components/Resolve/index.ts b/src/components/Resolve/index.ts
new file mode 100644
index 00000000..00a3eebd
--- /dev/null
+++ b/src/components/Resolve/index.ts
@@ -0,0 +1 @@
+export { default } from './Resolve';
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 2756655c..43dbb546 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1 +1,2 @@
export * from './isEmptyChildren';
+export * from './isPromise/isPromise';
diff --git a/src/utils/isPromise/isPromise.test.ts b/src/utils/isPromise/isPromise.test.ts
new file mode 100644
index 00000000..15c80f75
--- /dev/null
+++ b/src/utils/isPromise/isPromise.test.ts
@@ -0,0 +1,31 @@
+import { isPromise } from './isPromise';
+
+describe('isPromise type check', () => {
+ test('Should return positive if supplied a promise', () => {
+ const prom = new Promise(resolve => {
+ setTimeout(() => {
+ resolve(1);
+ }, 10);
+ });
+ const result = isPromise({ prom }, 'prom', 'component');
+ expect(result).toEqual(null);
+ });
+ test('Should return positive if supplied a nothing', () => {
+ const result = isPromise({}, 'prom', 'component');
+ expect(result).toEqual(null);
+ });
+ [
+ 'not a promise',
+ a => a,
+ true,
+ 1,
+ ['contains', 'not', 'a', 'promise'],
+ null,
+ undefined,
+ ].forEach(notPromise =>
+ test(`Should return negative if supplied a ${typeof notPromise}`, () => {
+ const result = isPromise({ prom: 'not a promise' }, 'prom', 'component');
+ expect(result).toMatchObject(new Error('prom in component is not a promise'));
+ }),
+ );
+});
diff --git a/src/utils/isPromise/isPromise.ts b/src/utils/isPromise/isPromise.ts
new file mode 100644
index 00000000..3e17a1dd
--- /dev/null
+++ b/src/utils/isPromise/isPromise.ts
@@ -0,0 +1,8 @@
+export const isPromise = (props: object, propName: string, componentName: string) => {
+ if (props[propName]) {
+ return props[propName] instanceof Promise
+ ? null
+ : new Error(`${propName} in ${componentName} is not a promise`);
+ }
+ return null;
+};