The analytics anti-pattern: every button and every form imports a track(...) helper, which imports the Segment SDK, which imports Amplitude, which is conditional on a feature flag — and the moment you want to swap providers, half your codebase changes. Triggery flips it: components fire a generic ui:event, a single trigger routes it to the right destination, and swapping providers is a one-file change.
A button is now just a button. Compare with the old import { track } from './analytics' style — the producer has no idea analytics exist beyond firing a generic event.
src/features/dashboard/DashboardButton.tsx
import { useEvent } from '@triggery/react';import { analyticsTrigger } from '../../triggers/analytics.trigger';export function DashboardButton() { const fireUiEvent = useEvent(analyticsTrigger, 'ui:event'); return ( <button type="button" onClick={() => { fireUiEvent({ name: 'dashboard:cta-clicked', props: { variant: 'hero' } }); // …business logic of the actual click here }} > Try it </button> );}
A single condition exposes which destinations are on. You can wire this to LaunchDarkly, your own flag service, a localStorage toggle, anything — the trigger doesn’t care.
src/features/analytics/FlagsProvider.tsx
import { useCondition } from '@triggery/react';import { analyticsTrigger, type Destinations } from '../../triggers/analytics.trigger';export function AnalyticsFlagsProvider({ destinations, children,}: { destinations: Destinations; children: React.ReactNode;}) { useCondition(analyticsTrigger, 'destinations', () => destinations, [destinations]); return <>{children}</>;}
Analytics calls are typically not on the critical path of a click — you’d like them off the main thread, but you don’t want them to delay rendering either. Triggery uses a microtask scheduler by default, which is the right answer here: the click handler returns synchronously, React re-renders, then the trigger runs.