Skip to content

Commit

Permalink
feat(core/presentation): Add refresh() api to useLatestPromise react …
Browse files Browse the repository at this point in the history
…hook (#7300)

- Migrate useLatestPromise return value from Array tuples to an object
with properties
  • Loading branch information
christopherthielen committed Aug 6, 2019
1 parent 1472b3d commit 3f5e4e6
Show file tree
Hide file tree
Showing 5 changed files with 34 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as React from 'react';

import { IFormInputProps, ReactSelectInput } from 'core/presentation';
import { useLatestPromise } from 'core/presentation/forms/useLatestPromise';
import { IFormInputProps, ReactSelectInput, useLatestPromise } from 'core/presentation';
import { ServiceAccountReader } from 'core/serviceAccount';

export function RunAsUserInput(props: IFormInputProps) {
const [serviceAccounts, status] = useLatestPromise(() => ServiceAccountReader.getServiceAccounts(), []);
const { result: serviceAccounts, status } = useLatestPromise(() => ServiceAccountReader.getServiceAccounts(), []);
const isLoading = status === 'PENDING';

return (
Expand Down
6 changes: 3 additions & 3 deletions app/scripts/modules/core/src/presentation/forms/FormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { isNil } from 'lodash';
import { IPromise } from 'angular';
import { $q } from 'ngimport';

import { noop } from '../../utils';
import { noop } from 'core/utils';
import { LayoutContext } from './layouts';
import { useLatestPromise } from './useLatestPromise';
import { useLatestPromise } from '../hooks';
import { createFieldValidator } from './FormikFormField';
import { renderContent } from './fields/renderContent';
import { IValidator, IValidatorResultRaw } from './validation';
Expand Down Expand Up @@ -55,7 +55,7 @@ export function FormField(props: IFormFieldProps) {
[label, required, validate],
);

const [errorMessage] = useLatestPromise(
const { result: errorMessage } = useLatestPromise(
// TODO: remove the following cast when we remove async validation from our API
() => $q.resolve((fieldValidator(value) as any) as IPromise<IValidatorResultRaw>),
[fieldValidator, value],
Expand Down
1 change: 1 addition & 0 deletions app/scripts/modules/core/src/presentation/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useLatestPromise';
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { DependencyList, useEffect, useRef, useState } from 'react';
import { IPromise } from 'angular';
import { DependencyList, useEffect, useRef, useState } from 'react';

export type IRequestStatus = 'NONE' | 'PENDING' | 'REJECTED' | 'RESOLVED';

export interface IUseLatestPromiseResult<T> {
// The value of the resolved promise returned from the callback
result: T;
// The status of the latest promise returned from the callback
status: IRequestStatus;
// The value of the rejected promise
error: any;
// A function that causes the callback to be invoked again
refresh: () => void;
// The current request ID -- could be used to count requests made, for example
requestId: number;
}

/**
* A react hook which invokes a callback that returns a promise.
* If multiple requests are made concurrently, only returns data from the latest request.
Expand All @@ -12,23 +25,30 @@ export type IRequestStatus = 'NONE' | 'PENDING' | 'REJECTED' | 'RESOLVED';
*
* @param callback a callback that returns an IPromise
* @param deps array of dependencies, which (when changed) cause the callback to be invoked again
* @returns an object with the result and current status of the promise
*/
export function useLatestPromise<T>(
callback: () => IPromise<T>,
deps: DependencyList,
): [T, IRequestStatus, any, number] {
export function useLatestPromise<T>(callback: () => IPromise<T>, deps: DependencyList): IUseLatestPromiseResult<T> {
const mounted = useRef(false);
const requestInFlight = useRef<IPromise<T>>();
const [status, setStatus] = useState<IRequestStatus>('NONE');
const [result, setResult] = useState<T>();
const [error, setError] = useState<any>();
const [requestId, setRequestId] = useState(0);

// Starts a new request (runs the callback again)
const refresh = () => setRequestId(currentRequestId => currentRequestId + 1);

// refresh whenever any dependency in the dependency list changes
useEffect(() => refresh(), deps);

// Manage the mount/unmounted state
useEffect(() => {
mounted.current = true;
return () => (mounted.current = false);
}, []);

// Invokes the callback and manages its lifecycle.
// This is triggered when the requestId changes
useEffect(() => {
const promise = callback();
const isCurrent = () => mounted.current === true && promise === requestInFlight.current;
Expand All @@ -39,7 +59,6 @@ export function useLatestPromise<T>(
}

setStatus('PENDING');
setRequestId(requestId + 1);
requestInFlight.current = promise;

const resolve = (newResult: T) => {
Expand All @@ -57,7 +76,7 @@ export function useLatestPromise<T>(
};

promise.then(resolve, reject);
}, deps);
}, [requestId]);

return [result, status, error, requestId];
return { result, status, error, refresh, requestId };
}
1 change: 1 addition & 0 deletions app/scripts/modules/core/src/presentation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './WatchValue';
export * from './collapsibleSection/CollapsibleSection';
export * from './details/Details';
export * from './forms';
export * from './hooks';
export * from './robotToHumanFilter/robotToHuman.filter';
export * from './sortToggle';
export * from './navigation';

0 comments on commit 3f5e4e6

Please sign in to comment.