Skip to content

Hooks & filters ​

Yatra is built around a wide hook surface: every meaningful state change fires an action, and most rendered values pass through a filter so you can override them. This page lists the most useful actions and filters grouped by area, plus general guidance on using them.

Yatra exposes hundreds of hooks. The list below covers the ~80 you'll reach for most often.

Verify hook names against your version

Hook names can be added, renamed, or retired between plugin versions. Before you build something that depends on a specific hook, confirm it exists in your version by searching the plugin source:

bash
grep -rh "do_action.*yatra_booking_created" wp-content/plugins/yatra wp-content/plugins/yatra-pro
grep -rh "apply_filters.*yatra_trip_price" wp-content/plugins/yatra

Every Yatra hook follows the yatra_* prefix.

Conventions ​

  • Actions are named yatra_{noun}_{verb} and fire after the underlying change (e.g. yatra_booking_created fires after the booking row is inserted).
  • Filters are named yatra_{noun}_{property} and pass the value as the first argument.
  • Pro hooks carry no special prefix — Pro just adds more yatra_* hooks. A few pro-specific hooks use yatra_pro_* (license, module activation).
  • Backwards compatibility: Yatra retires hooks with notice, never silently. Deprecated hooks keep firing for at least one minor version.

Trips ​

Fired around trip lifecycle and rendering.

HookTypePurpose
yatra_trip_createdactionAfter a trip is inserted; receives $trip_id
yatra_trip_updatedactionAfter a trip is updated
yatra_trip_deletedactionBefore a trip is deleted (soft or hard)
yatra_trip_loaded_with_relationsfilterMutate the joined trip object (with classifications, attributes, etc.)
yatra_trip_created_with_relationsactionAfter insert + relations save
yatra_trip_updated_with_relationsactionAfter update + relations save
yatra_trip_display_pricefilterLast-mile override for the displayed price
yatra_dynamic_pricing_enabledfilterToggle Pro Dynamic Pricing per-trip
yatra_availability_pricefilterOverride price on a specific departure
yatra_availability_horizon_monthsfilter(int $months) — How far ahead the storefront offers dates (the Booking horizon setting, default 12, clamped 1–36). Return a different value per request, e.g. add_filter('yatra_availability_horizon_months', fn($m) => is_page('summer') ? 6 : $m);. Out-of-range returns are ignored. Callers that pass an explicit to_date are not affected.
yatra_get_dynamic_pricing_display_settingsfilterCustomize how dynamic-pricing surcharges/discounts render
yatra_calculate_demand_scoresactionOverride demand-score calculation (Pro)

Bookings ​

Lifecycle and per-booking rendering.

HookTypePurpose
yatra_booking_createdactionNew booking submitted
yatra_booking_updatedactionBooking row updated
yatra_booking_status_changedactionEvery status transition. Args: ($bookingId, $oldStatus, $newStatus)
yatra_booking_confirmedactionBooking reached confirmed. Args: ($bookingId, $booking)
yatra_booking_cancelledactionBooking reached cancelled — admin, customer, unpaid-booking sweep or OTA. Args: ($bookingId, $booking)
yatra_booking_expiredactionAn unpaid booking was auto-cancelled by the expiry sweep. Args: ($bookingId)
yatra_auto_expire_bookingsfilterReturn false to disable the unpaid-booking expiry sweep
yatra_booking_deletedactionBefore a booking is deleted
yatra_booking_email_variablesfilterAugment the merge-tag map for booking emails
yatra_booking_email_traveler_identity_field_keysfilterCustomize identity-table fields
yatra_payment_completedactionFired across multiple gateways after capture

Booking pricing pipeline ​

The price you see on the booking summary is the result of a chain of filters. Hooking these lets you adjust the math without touching gateway code.

FilterStage
yatra_before_calculation_paramsNormalize incoming traveler counts, picked date, etc.
yatra_booking_trip_pricePer-trip base price
yatra_calculate_subtotalAfter traveler categories, before extras
yatra_booking_additional_servicesPro Additional Services subtotal
yatra_calculate_payment_amountsCompute deposit / partial / total payable now
yatra_after_calculation_resultFinal adjustments
yatra_booking_email_variablesVariables passed to email templates

The implementation lives in app/Services/CalculationService.php. Each filter receives the partial result and the booking context.

Payments ​

HookTypePurpose
yatra_register_payment_gatewaysactionRegister a custom gateway class
yatra_payment_gateway_config_savedactionAfter a gateway's settings save
yatra_before_payment_processingactionBefore a payment intent is created
yatra_price_unit_labelfilter(string $label, string $unit, ?array $priceType) — wording shown next to a price: "per person" or "per group (1-2 pax)" ($unit is per_person / per_group). Return e.g. "per tour".
yatra_payment_completedactionAfter successful capture. Since Yatra 3.0.15 also fired when an admin-recorded payment becomes Completed (payload adds payment_id, source => 'manual', send_emails)
yatra_send_manual_payment_emailsfilter(bool $send, int $bookingId, array $payment) — return false to keep the customer/admin "payment received" emails off for admin-recorded payments; yatra_payment_completed still fires with send_emails => false
yatra_payment_failedactionAfter failed capture
yatra_after_payment_processingactionFinal hook after payment processing succeeds
yatra_paypal_payment_completedactionPayPal-specific success
yatra_paypal_payment_refundedactionPayPal-specific refund (use this instead of a generic yatra_payment_refunded — Yatra fires per-gateway refund actions)
yatra_razorpay_refund_createdactionRazorpay-specific refund
yatra_payment_amount_mismatchaction(3.0.4+) Fires when a client-supplied amount in POST /payment/create-intent disagreed with the server's booking.amount_due. Args: ($booking_id, $client_amount, $server_amount). The transaction itself is forced to the server amount; this hook exists for fraud-monitoring integrations.
yatra_pdf_remote_enabledfilter(3.0.4+) Enable / disable dompdf remote image loading. Default true so PDFs can render the site logo and trip images. Return false to lock the PDF generator down to ABSPATH only (recommended if your invoices never contain external images).
yatra_pro_writable_settings_schemafilter(3.0.4+) Pro-only. Receives key => sanitizer-callable map of settings keys the POST /yatra/v1/settings REST endpoint is allowed to write. Modules can register their own keys here; anything not in this map is silently rejected by the endpoint.
yatra_pass_gateway_ids_for_scheduled_paymentsfilterWhether to forward the gateway's customer / payment-method IDs into the scheduled-payments pipeline even when save_card was off. Useful for gateways that auto-vault.
yatra_confirm_booking_on_paymentfilterOverride whether a successful payment auto-confirms the booking, on top of the Auto-Confirm mode. Args: ($shouldConfirm, bool $fullyPaid, int $bookingId, bool $autoConfirm). Default $shouldConfirm is true for mode all, or mode online when the balance is fully paid. $autoConfirm keeps its original meaning — the old on/off toggle, i.e. true only for mode all — so callbacks written against 3.0.10–3.0.14 behave the same; read the full mode with yatra_get_auto_confirm_mode() if you need to tell online from none. Example — confirm on any full payment regardless of mode: `add_filter('yatra_confirm_booking_on_payment', fn($c, $full) => $c

Stripe-specific actions (Pro) ​

Fired only when the verified Stripe webhook handler dispatches a known event. Verification is HMAC-SHA256 against the configured webhook_secret with a 5-minute replay window — handlers attached here only ever see real, signed Stripe events.

HookTypePurpose
yatra_stripe_payment_succeededaction(3.0.4+) payment_intent.succeeded. Receives the full PaymentIntent object as an array.
yatra_stripe_payment_failedaction(3.0.4+) payment_intent.payment_failed. Args: PaymentIntent object as array.
yatra_stripe_charge_refundedaction(3.0.4+) charge.refunded. Args: Charge object as array.

Adding a custom gateway ​

The minimal recipe:

php
add_action( 'yatra_register_payment_gateways', function ( $registry ) {
    $registry->register( 'my_gw', \My\Gateway\MyGateway::class );
} );

Your MyGateway extends \Yatra\PaymentGateways\AbstractPaymentGateway and implements id, name, description, getConfigFields, processPayment, optionally processRefund and handleWebhook. See app/PaymentGateways/Gateways/PayPal/PayPalGateway.php for a complete reference.

Email ​

HookTypePurpose
yatra_email_merge_tag_definitionsfilter(3.0.5+) Central merge-tag registry. Append / amend rows in Yatra\Services\EmailMergeTagRegistry. Tags added here automatically surface in the Pro Template Editor sidebar for every event they declare in events. Must be paired with a dispatcher-side injection (e.g. yatra_booking_email_variables) so the value actually renders.
yatra_email_template_variables_catalogfilter(3.0.5+) Legacy hook — applied AFTER the registry is grouped for the REST /email-templates/variables endpoint. Prefer yatra_email_merge_tag_definitions for new code.
yatra_email_event_variablesfilter(3.0.5+) Extend the per-event variable whitelist for a single event. Args: (array $keys, string $eventKey). Keys not already in the registry surface in an "event-specific" group on the sidebar.
yatra_send_transactional_emailfilterShort-circuit before sending; return false to suppress
yatra_pro_email_automation_owns_transactional_typefilterLet Pro Email Automation own a specific template type
yatra_booking_email_variablesfilterFinal mutation point for booking-event variables (after variablesFromBooking() + rich tags). Args: (array $variables, object $booking).
yatra_email_template_trip_variablesfilterAdd or rewrite the per-trip merge tags available to email templates
yatra_email_template_preview_variablesfilterSame, for the template preview pane
yatra_send_booking_status_email_htmlfilterRendered HTML for booking-status emails before send
yatra_email_template_enquiry_admin / _received / _responsefilterOverride the rendered HTML per enquiry email template

Adding a merge tag end-to-end ​

The registry is the single source of truth — adding a tag means registering it AND wiring up the dispatcher value. Skip step 2 and your tag will render as an empty string; skip step 1 and the sidebar won't surface it.

php
// 1. Declare the tag so the editor sidebar lists it for the right events.
add_filter( 'yatra_email_merge_tag_definitions', function ( array $catalog ) {
    $catalog['gst_invoice_number'] = [
        'key'         => 'gst_invoice_number',
        'label'       => 'GST Invoice Number',
        'description' => 'Tax-invoice number printed on the GST receipt.',
        'category'    => 'booking',
        'sample'      => 'GST-2025-00042',
        'events'      => [ 'booking.created', 'booking.confirmed', 'payment.received' ],
    ];
    return $catalog;
} );

// 2. Inject the real value so the rendered email actually contains it.
add_filter( 'yatra_booking_email_variables', function ( array $vars, $booking ) {
    $vars['gst_invoice_number'] = (string) get_post_meta( $booking->id, '_gst_invoice_number', true );
    return $vars;
}, 10, 2 );

Enquiries ​

HookTypePurpose
yatra_enquiry_createdactionAfter enquiry insert (carries the joined trip object since 3.0.3)
yatra_enable_enquiryfilterMaster toggle for enquiry UI on the trip page
yatra_enquiry_button_textfilterCustomise the "Make an Enquiry" CTA copy
yatra_enquiry_form_showfilterShow / hide the enquiry form per trip
yatra_enquiry_form_title_textfilterOverride the enquiry form title
yatra_email_template_enquiry_adminfilterRendered HTML for the admin enquiry notification
yatra_email_template_enquiry_responsefilterRendered HTML for the customer response email

Account / customer ​

HookTypePurpose
yatra_customer_createdactionNew customer record
yatra_customer_updatedactionAfter customer edit
yatra_user_idfilterResolve the user ID for a customer / booking flow
yatra_user_allowed_to_loginfilterBlock / allow a specific user from the front-end login flow
yatra_login_redirect_urlfilterRedirect destination after a successful login

Front-end routing & templates ​

Yatra registers its own pretty URLs for the booking flow, account, email verification and remaining-balance checkout.

HookTypePurpose
yatra_pretty_route_matchfilterModify how a route resolves (final say on tab/template)
yatra_plain_route_matchfilterSame but for ?yatra_* query-string URLs
yatra_frontend_request_pathfilterNormalize the request path before matching
yatra_register_rewrite_rulesactionAdd additional rewrite rules
yatra_build_archive_listing_urlfilterOverride how Yatra builds catalog listing URLs
yatra_get_trip_listing_urlfilterOverride the URL Yatra generates for the trip archive

Shortcodes and listings ​

HookTypePurpose
yatra_trip_listingfilterThe full listing object Yatra renders into the trip catalog
yatra_trip_listing_contextfilterThe context array passed to the trip card template
yatra_trip_listing_max_per_pagefilterCap on the per_page shortcode attribute
yatra_is_trip_listingfilterOverride Yatra's "are we on the trip-listing page" detection
yatra_listing_noncefilterCustomise the nonce field used inside listing-form posts

Want to customise card markup?

Yatra doesn't currently expose a stable *_card_html filter for whole-card replacement. The supported customisation paths are: (1) override the card template in your child theme — copy templates/partials/trip-card.php into wp-content/themes/{your-theme}/yatra/partials/trip-card.php; or (2) hook into the listing-context filter above to mutate the variables the card template sees.

Frontend templating & block themes (FSE) ​

Yatra renders its own URL paths (/trips/, /trip/{slug}/, /book/, /my-account/, etc.) with a custom router rather than via real WordPress post types. The following extension points exist so themes and other plugins can integrate cleanly — particularly with Full Site Editing block themes.

HookTypePurpose
pre_handle_404 (core hook, Yatra uses)filterYatra short-circuits this for its own URLs so WP::handle_404() never sets is_404 = true for plugin pages. Return true from your own callback (priority < 10) to opt a custom URL in.
body_classfilterYatra strips error404 for its pages and adds yatra-page plus a yatra-page-{type} class (e.g. yatra-page-trip).
template_includefilterYatra hooks at priority 99. If a request matched a Yatra route, the chosen PHP template is returned here. Customise via yatra_template_path below or with a theme override at your-theme/yatra/{template}.php.
yatra_template_pathfilter($absolute_path, $template_name, $page_type) — last chance to swap the PHP file before it's included. Useful for shipping a template variant from another plugin.
yatra_register_rewrite_rulesactionFires after Yatra registers its rewrite tags and rules. Receives the resolved permalink bases.

FSE / Site Editor ​

Yatra registers virtual block templates (Single Trip, Trip Listing, Destination, Activity, Booking, Booking Confirmation, My Account) so admins can find and customise Yatra layouts under Appearance → Editor → Templates.

  • The customisation is saved by core as a wp_template post. On the next request, Yatra detects it and renders that block template instead of the bundled PHP template.
  • Block templates embed a server-side block called Yatra Page Content (yatra/page-content). At render time it includes the PHP template selected by Yatra's router, so customising the chrome (header / sidebar / footer placement) is fully editable while the content stays driven by Yatra.
  • Inside that block, yatra_get_header() and yatra_get_footer() are no-ops — the canvas already emits the document and header/footer parts, so the PHP template only contributes its inner content.

Theme overrides without the editor

Drop a copy of any template at wp-content/themes/{your-theme}/yatra/{name}.php and locate_template() will pick it up first. No filter wiring needed. This works for classic and block themes alike.

Admin / settings / modules ​

HookTypePurpose
yatra_admin_localized_datafilterInject extra props into window.yatraAdmin. Pro modules use this to expose flags like flexiblePaymentsEnabled so the React admin can conditionally render Pro-gated UI.
yatra_module_activeactionWhen a module is enabled
yatra_module_deactiveactionWhen a module is disabled
yatra_module_enabled_statusfilterAuthoritative is-this-module-enabled check
yatra_module_capabilitiesfilterModule-required capabilities
yatra_module_assetsfilterAssets enqueued for a module
yatra_default_modulesfilterAdd to / remove from the default module registry
yatra_enable_setup_wizardfilterShow / hide the setup wizard
yatra_clear_cacheactionInvalidate Yatra's internal cache

Pro license & updater ​

HookTypePurpose
yatra_is_pro_activefilterToggle Pro behavior (Pro returns true; you can short-circuit for testing)
yatra_pro_available_modulesfilterAdd or remove modules from the Modules screen
yatra_pro_license_store_urlfilterOverride the license store URL
yatra_pro_license_item_idfilterOverride the EDD product ID
yatra_pro_license_data_updatedactionAfter license data is saved
yatra_pro_module_activatedactionAfter a Pro module activates (creates tables, etc.)
yatra_pro_module_settings_updatedactionAfter a Pro module's settings save

Pro: Dynamic Pricing ​

HookTypePurpose
yatra_dynamic_pricing_enabledfilterWhether DP applies to a given trip
yatra_get_dynamic_pricing_display_settingsfilterDisplay settings for surcharge/discount badges
yatra_price_breakdownfilterThe breakdown shown on the booking summary
yatra_calculate_demand_scoresactionOverride / extend demand calculation

Pro: Flexible / Scheduled Payments ​

HookTypePurpose
yatra_flexible_payments_enabledfilterMaster toggle (per-trip or globally)
yatra_flexible_payment_settingfilterRead individual setting values
yatra_deposit_percentagefilter($default, $context = []) — Override deposit %. $context['trip_id'] lets the Pro module honour per-trip trip.deposit_percentage
yatra_partial_payment_percentagefilter($default, $context = []) — Override partial-payment % (context unused today; passed for parity)
yatra_calculate_amount_duefilter($amount_due, $total_amount, $payment_method, $context = []) — Final say on the payable-now amount. $context['trip_id'] lets Pro apply per-trip trip.deposit_amount (absolute) or trip.deposit_percentage
yatra_payment_method_optionsfilter($options, $booking_data) — Adds Pay X% Deposit / Pay X% Now radios to the booking form. $booking_data['trip_id'] lets Pro show the deposit option whenever a trip has per-trip values, even with the global flag off
yatra_scheduled_payments_module_activefilterWhether scheduled payments processing should run
yatra_scheduled_payment_settingfilterPer-setting reads
yatra_scheduled_payment_successactionA scheduled (installment / balance) charge succeeded. Args: (int $scheduledPaymentId, array $gatewayResult, array $context). The first arg is the scheduled-payment row id, not the booking — read $context['booking_id']. $context also carries scheduled_payment_id, payment_id, amount, currency, gateway, method, transaction_id, payment_type, amount_due, fully_paid, plus the display-ready scheduled_amount_formatted, scheduled_date_formatted, payment_type_label, balance_after_formatted (formatted identically to the transactional email). Webhooks, WhatsApp and Email Automation bind their Scheduled payment succeeded event here.
yatra_scheduled_payment_failedactionA scheduled charge failed ($context['permanent'] is true once max attempts is reached). Args: (int $scheduledPaymentId, string $error, array $context) — the first two are unchanged from earlier releases; $context (booking_id, scheduled_payment_id, reason, error, failure_reason, permanent, gateway, amount, currency, payment_type, scheduled_amount_formatted, scheduled_date_formatted, payment_type_label) is new. Read the booking from $context['booking_id'], never from the first arg.

Per-trip overrides

The $context argument added to yatra_deposit_percentage, yatra_calculate_amount_due, and yatra_payment_method_options since Yatra Free 3.0.5 / Pro 3.0.3 is what lets the Flexible Payments module read trip.deposit_amount / trip.deposit_percentage for a specific booking. If you implement your own filter callback that ignores $context, you'll only get site-wide behaviour — which is fine, but match the new signature if you want trip-aware behaviour.

Pro: Email Automation ​

HookTypePurpose
yatra_pro_email_automation_owns_transactional_typefilterLet Pro Email Automation own a specific template type so Pro's send pipeline runs instead of the free one

Trip-specific override templates (Pro Email Automation) need no new hook: the module resolves the override inside its existing yatra_send_transactional_email takeover, using the trip_id merge variable the free plugin now puts in every booking email's variables (TransactionalEmailTemplateService::variablesFromBooking(), free 3.0.16+; older free plugins are handled by looking the trip up from booking_id). An override is a normal yatra_email_templates row whose settings JSON carries overrides (the global key), targets (trips / categories / trip_types) and priority; its event_key is stored empty so older Pro builds ignore it. REST: POST /email-templates/{id}/override, POST /email-templates/overrides/reorder, GET /email-templates/resolve?trip_id=N; preview and test accept trip_id.

Looking for sequence-step or payload filters?

Yatra's source emits sequence events through the email-template filter chain (see the Email section above). If you need pre-send mutation, hook into yatra_send_transactional_email (returns false to suppress) or the per-template filters like yatra_email_template_trip_variables.

Pro: Custom Landing Pages ​

HookTypePurpose
yatra_destination_permalinkfilterOverride the destination URL
yatra_activity_permalinkfilterOverride the activity URL
yatra_category_permalinkfilterOverride the trip-category URL
HookTypePurpose
yatra_consent_signedactionAfter a customer signs a consent form

Customise consent email copy

The consent-request email is a normal Yatra template (trip_consent_request). Override the body via Settings → Email → Templates, or hook into yatra_email_template_trip_variables to rewrite the merge-tag values it receives.

Pro: Dynamic Form Field ​

HookTypePurpose
yatra_booking_form_configfilterThe merged booking-form configuration as read by checkout, validation and the admin. Args: (array $config, ?int $tripId). $tripId (free 3.0.16+) is the trip being booked, or null when there is no trip context (the Settings editor). The Pro module uses it to resolve each section's per-trip conditions — the first condition matching the trip supplies the section's title, description and fields, and the raw conditions list is removed from the resolved config. Callbacks registered with one argument keep working.
yatra_save_booking_form_configfilterRuns on save, after the free sanitiser. Args: (array $sanitized, array $original). Use it to persist your own per-field keys from $original.
yatra_dynamic_form_field_enabledfilterModule on/off as seen by the free plugin (window.yatraAdmin.dynamicFormFieldEnabled).

A section's conditions are stored as conditions => [ [ 'id', 'targets' => ['trips' => [ids], 'categories' => [ids], 'trip_types' => ['single_day'|'multi_day']], 'title', 'description', 'fields' => [...] ], ... ] and sanitised by the free plugin like the section's own fields. Read a trip's resolved config with yatra_get_booking_form_config( $trip_id ) or GET /yatra/v1/settings/booking-form?trip_id=N (readable by anyone who can view bookings); the argument-less call / request returns the global config with its conditions.

Pro: Additional Services ​

HookTypePurpose
yatra_booking_additional_servicesfilterMutate selected services on a booking
yatra_booking_save_servicesactionAfter services are persisted
yatra_booking_get_servicesfilterModify how services are fetched for display

Practical recipes ​

Send a Slack notification on every booking ​

php
add_action( 'yatra_booking_created', function ( $booking_id ) {
    $booking = yatra_get_booking( $booking_id );
    if ( ! $booking ) return;

    wp_remote_post( SLACK_WEBHOOK_URL, [
        'body'    => wp_json_encode( [
            'text' => sprintf(
                'New booking %s for %s · %d travelers · %s',
                $booking->reference,
                $booking->trip_title,
                $booking->traveler_count,
                yatra_price_html( $booking->total )
            ),
        ] ),
        'headers' => [ 'Content-Type' => 'application/json' ],
    ] );
}, 10, 1 );

Add 5% surcharge to all bookings on weekends ​

php
add_filter( 'yatra_booking_trip_price', function ( $price, $context ) {
    $departure = $context['departure_date'] ?? null;
    if ( ! $departure ) return $price;

    $day = (int) date( 'N', strtotime( $departure ) );
    if ( $day >= 6 ) {                  // Sat / Sun
        $price *= 1.05;
    }
    return $price;
}, 10, 2 );

Hide trips from a specific destination on a public listing ​

php
add_filter( 'yatra_trip_listing_filters', function ( $filters ) {
    if ( is_admin() || ! empty( $filters['private_view'] ) ) return $filters;

    $filters['exclude_destination'] = [ 99 ];   // hide destination ID 99 from public listings
    return $filters;
} );

Auto-tag Mailchimp subscribers based on the trip booked ​

php
add_action( 'yatra_booking_created', function ( $booking_id ) {
    $booking = yatra_get_booking( $booking_id );
    if ( ! $booking ) return;

    $tag = sprintf( 'trip-%s', sanitize_title( $booking->trip_title ) );
    do_action( 'yatra_mailchimp_add_tag', $booking->customer_email, $tag );
} );

Block weekend bookings of a specific trip ​

php
add_filter( 'yatra_resolve_availability_object', function ( $availability, $trip_id, $date ) {
    if ( $trip_id !== 42 ) return $availability;
    $day = (int) date( 'N', strtotime( $date ) );
    if ( $day >= 6 ) {
        $availability['available'] = false;
        $availability['message']   = 'This trip runs Mon–Fri only.';
    }
    return $availability;
}, 10, 3 );

Discovering more hooks ​

bash
cd wp-content/plugins/yatra
# All actions:
grep -rn "do_action\(\s*'yatra_" app/ | sort -t: -k1
# All filters:
grep -rn "apply_filters\(\s*'yatra_" app/ | sort -t: -k1

Most hooks have a short docblock explaining the arguments and intent. The cleanest summary lives at the call site rather than in a separate hook reference, so reading the source is the canonical move when you're stuck.