Skip to content

Instantly share code, notes, and snippets.

@mnixry
Last active April 28, 2025 00:33
Show Gist options
  • Save mnixry/2e29f83e009bd36e070eb0c13b09d950 to your computer and use it in GitHub Desktop.
Save mnixry/2e29f83e009bd36e070eb0c13b09d950 to your computer and use it in GitHub Desktop.
Loading & Refresh Button component for Shadcn UI & Lucide Icons.
import { useCallback, useState } from 'react';
import { Loader2Icon } from 'lucide-react';
import { useInterval } from 'react-use';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
export const LoadingButtonPrimitive: React.FC<
React.ComponentPropsWithRef<typeof Button> & { loading?: boolean }
> = ({ loading, disabled, className, children, ...props }) => {
return (
<Button
disabled={loading || disabled}
className={cn(
'disabled:pointer-events-auto',
loading ? 'disabled:cursor-wait' : 'disabled:cursor-not-allowed',
className
)}
{...props}
>
{children}
</Button>
);
};
interface LoadingButtonProps {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;
leftSection?: React.ReactNode;
leftSectionLoading?: React.ReactNode;
childrenLoading?: React.ReactNode;
}
export const LoadingButton: React.FC<
Omit<React.ComponentPropsWithRef<typeof LoadingButtonPrimitive>, keyof LoadingButtonProps> &
LoadingButtonProps
> = ({
onClick: onClickProp,
leftSection,
leftSectionLoading,
childrenLoading,
children,
...props
}) => {
const [loading, setLoading] = useState(false);
const onClick = useCallback(
async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
try {
setLoading(true);
await onClickProp?.(e);
} finally {
setLoading(false);
}
},
[onClickProp]
);
return (
<LoadingButtonPrimitive onClick={onClick} loading={loading} {...props}>
{loading
? (leftSectionLoading ?? <Loader2Icon className="mr-2 size-4 animate-spin" />)
: leftSection}
{loading && childrenLoading ? childrenLoading : children}
</LoadingButtonPrimitive>
);
};
const ConfirmInnerDefault: React.FC<{ remaining?: number }> = ({ remaining }) => {
return <span>Proceed? {typeof remaining === 'number' ? `(${remaining}s)` : ''}</span>;
};
interface DestructiveLoadingButtonProps extends LoadingButtonProps {
confirmInner?: React.ComponentType<React.ComponentProps<typeof ConfirmInnerDefault>>;
}
export const DestructiveLoadingButton: React.FC<
Omit<
React.ComponentPropsWithRef<typeof LoadingButtonPrimitive>,
keyof DestructiveLoadingButtonProps
> &
DestructiveLoadingButtonProps
> = ({
onClick: onClickProp,
leftSection,
leftSectionLoading,
childrenLoading,
confirmInner: ConfirmInner = ConfirmInnerDefault,
children: childrenProp,
variant,
disabled,
...props
}) => {
const [loading, setLoading] = useState(false);
const [confirming, setConfirming] = useState(false);
const [protectiveDisabled, setProtectiveDisabled] = useState<number>();
useInterval(
() => {
setProtectiveDisabled((prev) =>
typeof prev === 'number' && prev - 1 > 0 ? prev - 1 : undefined
);
},
typeof protectiveDisabled === 'number' ? 1000 : null
);
const onClick = useCallback(
async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
if (!confirming) {
setConfirming(true);
setProtectiveDisabled(3);
setTimeout(() => setConfirming(false), 10 * 1000);
return;
}
try {
setConfirming(false);
setLoading(true);
await onClickProp?.(e);
} finally {
setLoading(false);
}
},
[onClickProp, confirming]
);
const children = loading ? (
<>
{leftSectionLoading ?? <Loader2Icon className="mr-2 size-4 animate-spin" />}\
{childrenLoading ?? childrenProp}
</>
) : confirming ? (
<>
{leftSection}
<ConfirmInner remaining={protectiveDisabled} />
</>
) : (
<>
{leftSection}
{childrenProp}
</>
);
return (
<LoadingButtonPrimitive
loading={loading}
variant={confirming ? 'destructive' : variant}
disabled={disabled || typeof protectiveDisabled === 'number'}
onClick={onClick}
{...props}
>
{children}
</LoadingButtonPrimitive>
);
};
import { useCallback, useEffect, useState } from 'react';
import { RefreshCw } from 'lucide-react';
import { LoadingButtonPrimitive } from '@/components/loading-button';
export const RefreshButton: React.FC<
React.ComponentProps<typeof LoadingButtonPrimitive> & {
isRefreshing?: boolean;
}
> = ({ children, isRefreshing, ...rest }) => {
const [isAnimating, setIsAnimating] = useState(false);
const [rotation, setRotation] = useState(0);
const onTransitionEnd = useCallback(() => {
if (isRefreshing) {
setIsAnimating(true);
setRotation((prev) => prev + 360);
} else {
setIsAnimating(false);
}
}, [isRefreshing, setRotation]);
useEffect(() => {
onTransitionEnd();
}, [onTransitionEnd]);
return (
<LoadingButtonPrimitive variant="ghost" size="icon" loading={isAnimating} {...rest}>
<RefreshCw
className="size-4 duration-1000 transition-transform"
style={{
transform: `rotate(${rotation}deg)`,
}}
onTransitionEnd={onTransitionEnd}
/>
{children}
</LoadingButtonPrimitive>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment