LoadingButton Usage Pattern
This skill ensures consistent async button implementation using the LoadingButton component.
When to Apply
Use the LoadingButton component for any button that performs:
- Form submissions
- API calls
- Data mutations (create, update, delete)
- File uploads
- Any asynchronous operation
Implementation Requirements
1. Import the Component
Always import both the component and type:
import { LoadingButton } from '@/components/ui/loading-button';
import type { ButtonLoadingState } from '@/components/ui/loading-button';
2. State Management
Define the button state using the correct pattern:
Single Button:
const [buttonState, setButtonState] = useState<ButtonLoadingState>('default');
Multiple Buttons: Name states according to their operation:
const [saveButtonState, setSaveButtonState] = useState<ButtonLoadingState>('default');
const [deleteButtonState, setDeleteButtonState] = useState<ButtonLoadingState>('default');
const [submitButtonState, setSubmitButtonState] = useState<ButtonLoadingState>('default');
3. Button States
The ButtonLoadingState type has 5 possible values:
'default'- Initial/idle state'loading'- During async operation (shows spinner)'success'- Operation completed successfully (shows check mark)'error'- Operation failed'disabled'- Button is disabled
4. Basic Usage Example
const [buttonState, setButtonState] = useState<ButtonLoadingState>('default');
const handleSubmit = async () => {
setButtonState('loading');
try {
await someAsyncOperation();
setButtonState('success');
} catch (error) {
setButtonState('error');
console.error(error);
}
};
return (
<LoadingButton
buttonState={buttonState}
setButtonState={setButtonState}
text="Submit"
loadingText="Submitting..."
successText="Submitted!"
errorText="Failed to submit"
/>
);
5. Auto-Reset Feature
Enable auto-reset to return to 'default' state after success:
<LoadingButton
buttonState={buttonState}
setButtonState={setButtonState}
reset={true} // Resets to 'default' after 2 seconds
text="Save"
loadingText="Saving..."
successText="Saved!"
/>
Important: When reset={true}, you must provide setButtonState.
6. Required Props
Minimum required props:
buttonState- The current statetext- Default button text
7. Optional Props
Enhance UX with custom text for each state:
loadingText- Text/node shown during loading (default shows spinner only)successText- Text/node shown on success (default shows check mark only)errorText- Text shown on errorsetButtonState- State setter (required if usingreset)reset- Auto-reset to default after 2 seconds on success- Standard button props:
variant,size,className,disabled, etc.
Common Patterns
Form Submission
const [submitButtonState, setSubmitButtonState] = useState<ButtonLoadingState>('default');
const (data: FormData) => {
setSubmitButtonState('loading');
try {
const result = await createItem(data);
setSubmitButtonState('success');
} catch (error) {
setSubmitButtonState('error');
toast.error('Failed to create item');
}
};
Delete Action
const [deleteButtonState, setDeleteButtonState] = useState<ButtonLoadingState>('default');
const handleDelete = async () => {
setDeleteButtonState('loading');
try {
await deleteItem(itemId);
setDeleteButtonState('success');
router.push('/items');
} catch (error) {
setDeleteButtonState('error');
}
};
Multiple Actions on Same Form
const [saveButtonState, setSaveButtonState] = useState<ButtonLoadingState>('default');
const [publishButtonState, setPublishButtonState] = useState<ButtonLoadingState>('default');
const handleSave = async (draft: boolean) => {
const setState = draft ? setSaveButtonState : setPublishButtonState;
setState('loading');
try {
await savePost({ ...data, draft });
setState('success');
} catch (error) {
setState('error');
}
};
Migration from Standard Button
When you see a regular Button handling async operations:
Before:
<Button disabled={isLoading}>
{isLoading ? 'Loading...' : 'Submit'}
</Button>
After:
const [buttonState, setButtonState] = useState<ButtonLoadingState>('default');
const handleSubmit = async () => {
setButtonState('loading');
try {
await operation();
setButtonState('success');
} catch (error) {
setButtonState('error');
}
};
<LoadingButton
buttonState={buttonState}
setButtonState={setButtonState}
text="Submit"
loadingText="Loading..."
reset={true}
/>
What to Check
When reviewing code or implementing buttons:
- ✅ Does the button perform an async operation?
- ✅ Is LoadingButton imported correctly?
- ✅ Is ButtonLoadingState type imported?
- ✅ Is state defined with correct type annotation?
- ✅ Are multiple button states named descriptively?
- ✅ Does the async handler update state correctly (loading → success/error)?
- ✅ Is error handling in place?
- ✅ Is
setButtonStateprovided when usingreset={true}?
Component Reference
Location: components/ui/loading-button.tsx
The LoadingButton is built on top of the base Button component and adds:
- Automatic loading spinner (Loader2Icon)
- Success checkmark (Check icon)
- State-based text switching
- Auto-reset functionality
- Proper disabled state management
Common Mistakes to Avoid
❌ Forgetting to set state to 'loading':
// Wrong - button won't show loading state
const handleClick = async () => {
await operation();
setButtonState('success');
};
❌ Not handling errors:
// Wrong - button stays in loading state on error
const handleClick = async () => {
setButtonState('loading');
await operation(); // If this fails, state never updates
setButtonState('success');
};
❌ Using reset without setButtonState:
// Wrong - will throw error
<LoadingButton
buttonState={buttonState}
reset={true} // Requires setButtonState!
text="Save"
/>
❌ Generic state names with multiple buttons:
// Wrong - confusing when multiple buttons exist
const [buttonState1, setButtonState1] = useState<ButtonLoadingState>('default');
const [buttonState2, setButtonState2] = useState<ButtonLoadingState>('default');
// Better
const [saveButtonState, setSaveButtonState] = useState<ButtonLoadingState>('default');
const [deleteButtonState, setDeleteButtonState] = useState<ButtonLoadingState>('default');
Implementation Checklist
When implementing or reviewing LoadingButton usage:
- Imported LoadingButton component
- Imported ButtonLoadingState type
- Defined state with proper type annotation
- State names are descriptive (if multiple buttons)
- Set state to 'loading' before async operation
- Set state to 'success' after successful operation
- Set state to 'error' in catch block
- Provided meaningful text for each state
- Added error handling/logging
- Used
resetprop if auto-reset is desired - Provided
setButtonStatewhen usingreset