Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/wojtekmaj/react-async-button. A button that handles Promises for your React app.
Something went wrong. Try again.
3.9 kB · 153 lines
TSX
at attw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154import React, { useCallback, useEffect, useRef, useState } from 'react';import PropTypes from 'prop-types';import makeCancellable from 'make-cancellable-promise';
type Config<T extends React.ElementType> = Omit< React.ComponentPropsWithoutRef<T>, 'as' | 'onClick'>;
type AsyncMaybe<T extends ((...args: any[]) => any) | unknown> = T extends (...args: any[]) => any ? (...args: Parameters<T>) => ReturnType<T> | Promise<ReturnType<T>> : never;
type OnClickType<T extends React.ElementType> = React.ComponentPropsWithoutRef<T>['onClick'];
export type AsyncButtonProps<T extends React.ElementType> = { as?: T; errorConfig?: Config<T>; onClick?: AsyncMaybe<OnClickType<T>>; pendingConfig?: Config<T>; resetTimeout?: number; successConfig?: Config<T>;} & Config<T>;
type PolymorphicRef<T extends React.ElementType> = React.ComponentPropsWithRef<T>['ref'];
const STATES = { ERROR: 'error', INIT: 'init', PENDING: 'pending', SUCCESS: 'success',} as const;
const AsyncButton = React.forwardRef( <T extends React.ElementType = 'button'>( { as, errorConfig, onClick, pendingConfig, resetTimeout = 2000, successConfig, ...otherProps }: AsyncButtonProps<T>, ref?: PolymorphicRef<T>, ) => { const [buttonState, setButtonState] = useState<(typeof STATES)[keyof typeof STATES]>( STATES.INIT, ); const cancellablePromise = useRef<ReturnType<typeof makeCancellable>>(); const timeout = useRef<ReturnType<typeof setTimeout>>();
useEffect( () => () => { if (cancellablePromise.current) { cancellablePromise.current.cancel(); } clearTimeout(timeout.current); }, [], );
const onClickInternal = useCallback( (event: React.MouseEvent) => { if (!onClick) { return; }
clearTimeout(timeout.current);
const onSuccess = () => { setButtonState(STATES.SUCCESS); };
const onError = () => { setButtonState(STATES.ERROR); };
const finallyCallback = () => { timeout.current = setTimeout(() => { setButtonState(STATES.INIT); }, resetTimeout); };
try { const result = onClick(event); setButtonState(STATES.PENDING);
if (result instanceof Promise) { cancellablePromise.current = makeCancellable(result); cancellablePromise.current.promise .then(onSuccess) .catch(onError) .finally(finallyCallback); } else { onSuccess(); finallyCallback(); } } catch (error) { onError(); finallyCallback(); } }, [onClick, resetTimeout], );
const Component = as || 'button';
const buttonConfig: Config<T> | null | undefined = (() => { switch (buttonState) { case STATES.ERROR: return errorConfig; case STATES.PENDING: return pendingConfig; case STATES.SUCCESS: return successConfig; default: return null; } })();
return ( <Component ref={ref} onClick={onClick ? onClickInternal : undefined} {...otherProps} {...buttonConfig} /> ); },);
AsyncButton.displayName = 'AsyncButton';
const configProps = { children: PropTypes.node, className: PropTypes.string,};
const isConfigObject = PropTypes.shape(configProps);
AsyncButton.propTypes = { ...configProps, errorConfig: isConfigObject, onClick: PropTypes.func, pendingConfig: isConfigObject, resetTimeout: PropTypes.number, successConfig: isConfigObject,};
export default AsyncButton as <T extends React.ElementType = 'button'>( props: AsyncButtonProps<T> & { ref?: PolymorphicRef<T> },) => React.ReactElement | null;