Skip to main content

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:

  1. Code Duplication: Each mutation hook implemented its own:

    • Toast notifications on success/error
    • Query invalidation logic
    • Error handling patterns
    • Retry configuration
  2. Inconsistent Patterns: Different hooks used different approaches:

    • Some used toast.success(), others used custom messages
    • Query invalidation patterns varied
    • Error handling was inconsistent
  3. Maintenance Overhead: Making changes to mutation behavior required updating many files

Decision

Create a centralized mutation utility hook that standardizes common patterns.

Implementation

  1. Centralized Hook: useMutationWithToast

    • Handles toast notifications automatically
    • Manages query invalidation
    • Provides consistent error handling
    • Configurable success/error messages
  2. 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

  1. GraphQL Organization Hooks:

    • useUpdateOrganization
  2. Billing Hooks:

    • Subscription Management:
      • useCreateSubscription
      • useUpdateSubscription
      • useCancelSubscription
    • Payment Methods:
      • useCreateSetupIntent (found during audit)
      • useSetDefaultPaymentMethod
      • useDetachPaymentMethod
      • useCreatePortalSession
    • Alerts:
      • useCreateBillingAlert
      • useUpdateBillingAlert
      • useDeleteBillingAlert
    • Usage & Limits (found during audit):
      • useReportUsage
      • useSetSpendingLimit
    • Organization (found during audit):
      • useUpdateBillingDetails
    • Checkout (found during audit):
      • useCreateCheckoutSession
  3. Stripe Hooks:

    • useCreateSubscription
    • useCancelSubscription
    • useCreateSetupIntent
    • useUpdateDefaultPaymentMethod
    • useRemovePaymentMethod
    • useCreatePortalSession

Files Removed

  • /src/hooks/use-organization.deprecated.ts - No longer needed
  • ADR-037: Comprehensive Code Deduplication
  • ADR-038: Edge Function Wrapper Pattern
  • ADR-039: GraphQL Snake Case Standardization