Source:
ocean/docs/adr/ADR-040-react-hook-mutation-pattern.md| ✏️ Edit on GitHub
ADR-040: React Hook Mutation Pattern Standardization
Status
Accepted
Context
Our React hooks for mutations had several issues:
-
Code Duplication: Each mutation hook implemented its own:
- Toast notifications on success/error
- Query invalidation logic
- Error handling patterns
- Retry configuration
-
Inconsistent Patterns: Different hooks used different approaches:
- Some used
toast.success(), others used custom messages - Query invalidation patterns varied
- Error handling was inconsistent
- Some used
-
Maintenance Overhead: Making changes to mutation behavior required updating many files
Decision
Create a centralized mutation utility hook that standardizes common patterns.
Implementation
-
Centralized Hook:
useMutationWithToast- Handles toast notifications automatically
- Manages query invalidation
- Provides consistent error handling
- Configurable success/error messages
-
Features:
export interface UseMutationWithToastOptions<TData, TError, TVariables> {
mutationFn: (variables: TVariables) => Promise<TData>
successMessage?: string | ((data: TData, variables: TVariables) => string)
errorMessage?: string
invalidateQueries?: Array<string[]>
onSuccess?: (data: TData, variables: TVariables, context: unknown) => void
onError?: (error: TError, variables: TVariables, context: unknown) => void
retry?: UseMutationOptions<TData, TError, TVariables>['retry']
}
Consequences
Positive
- Reduced Code Duplication: ~40% reduction in mutation hook code
- Consistent UX: All mutations now have consistent toast notifications
- Easier Maintenance: Changes to mutation behavior only require updating one file
- Better Error Handling: Centralized error handling with
toastError - Flexible: Can still customize behavior through callbacks
Negative
- Additional Abstraction: One more layer of abstraction to understand
- Migration Effort: Required updating all existing mutation hooks
Migration Example
Before:
export function useUpdateOrganization() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, updates }) => {
// mutation logic
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['organization', data.id] })
queryClient.invalidateQueries({ queryKey: ['organizations'] })
toast.success('Organization updated')
},
onError: (error) => {
toastError(error, 'Failed to update organization')
},
retry: false,
})
}
After:
export function useUpdateOrganization() {
return useMutationWithToast({
mutationFn: async ({ id, updates }) => {
// mutation logic
},
successMessage: 'Organization updated successfully',
errorMessage: 'Failed to update organization',
invalidateQueries: [['organizations']],
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['organization', data.id] })
},
})
}
Technical Details
Hooks Refactored
-
GraphQL Organization Hooks:
useUpdateOrganization
-
Billing Hooks:
- Subscription Management:
useCreateSubscriptionuseUpdateSubscriptionuseCancelSubscription
- Payment Methods:
useCreateSetupIntent(found during audit)useSetDefaultPaymentMethoduseDetachPaymentMethoduseCreatePortalSession
- Alerts:
useCreateBillingAlertuseUpdateBillingAlertuseDeleteBillingAlert
- Usage & Limits (found during audit):
useReportUsageuseSetSpendingLimit
- Organization (found during audit):
useUpdateBillingDetails
- Checkout (found during audit):
useCreateCheckoutSession
- Subscription Management:
-
Stripe Hooks:
useCreateSubscriptionuseCancelSubscriptionuseCreateSetupIntentuseUpdateDefaultPaymentMethoduseRemovePaymentMethoduseCreatePortalSession
Files Removed
/src/hooks/use-organization.deprecated.ts- No longer needed
Related ADRs
- ADR-037: Comprehensive Code Deduplication
- ADR-038: Edge Function Wrapper Pattern
- ADR-039: GraphQL Snake Case Standardization