Skip to content

Patching Components

The patch system allows modules to extend or modify components defined in other modules without changing their source code. This is essential for addon modules that need to enhance existing functionality.

When you patch a component:

  1. Framework stores the patch with its options (priority, conditions, dependencies)
  2. Patches are sorted by priority and dependency order
  3. Each patch wraps the previous result, starting from the original
  4. The final patched component is stored in the registry
  5. All instances are re-mounted with the patched version
Original Component Patch A (priority: 10) Patch B (priority: 20)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ProductCard │ → │ + Discount │ → │ + Loyalty │
│ │ │ Badge │ │ Points │
└─────────────────┘ └─────────────────┘ └─────────────────┘

Add content around an existing component:

const { patch, html } = window.fullfinity;
patch('ProductCard', (Original) => function(props) {
return html`
<div class="product-card-wrapper">
<${Original} ...${props} />
<div class="loyalty-badge">
+10 points with purchase
</div>
</div>
`;
});
patch('ProductCard', (Original) => function(props) {
return html`
<${Original} ...${props} />
<div class="loyalty-badge">+10 pts</div>
`;
}, {
id: 'loyalty-badge', // Unique identifier (required for unpatch/after)
priority: 20, // Lower runs first (default: 10)
});

Patches are applied in priority order (lower number = earlier):

// This runs second (priority 20)
patch('Cart', addLoyaltyDisplay, { id: 'loyalty', priority: 20 });
// This runs first (priority 10)
patch('Cart', addDiscountBadge, { id: 'discount', priority: 10 });
// Result: Original → Discount → Loyalty

When priorities are equal, patches apply in registration order (module load order).

Use after to ensure one patch runs after another:

// Patch A - no dependencies
patch('Checkout', addCouponField, { id: 'coupon-field' });
// Patch B - must run after coupon-field
patch('Checkout', addCouponValidation, {
id: 'coupon-validation',
after: 'coupon-field' // Runs after coupon-field regardless of priority
});
// Multiple dependencies
patch('Checkout', addCouponSummary, {
id: 'coupon-summary',
after: ['coupon-field', 'coupon-validation']
});

Apply patches only when conditions are met:

patch('CheckoutButton', addSubscriptionUpsell, {
id: 'subscription-upsell',
when: () => !window.currentUser?.hasSubscription
});
// Patch is evaluated each time component rebuilds
patch('PriceDisplay', addMemberDiscount, {
id: 'member-discount',
when: () => window.currentUser?.isMember
});

The when function is called during component rebuild. If it returns false, the patch is skipped.

Use unpatch() to remove a patch at runtime:

const { patch, unpatch } = window.fullfinity;
// Add a patch
patch('Header', addPromoBanner, { id: 'promo-banner' });
// Later, remove it
unpatch('Header', 'promo-banner');

This rebuilds the component without the removed patch.

See all patches applied to a component:

const { listPatches } = window.fullfinity;
console.log(listPatches('ProductCard'));
// ['discount-badge', 'loyalty-badge', 'inventory-status']

For class-based or object components, patch specific methods with _super support. Supports deferred patching (can patch before component exists) and multiple patches with priority.

const { patchMethod } = window.fullfinity;
// Patch a method with access to original via _super
patchMethod('CartWidget', 'computeTotal', function(_super, items) {
const total = _super(items); // Call original
return total * 0.9; // Apply 10% discount
}, { id: 'discount', priority: 10 });
// Multiple patches stack - this runs after discount (higher priority = later)
patchMethod('CartWidget', 'computeTotal', function(_super, items) {
const total = _super(items); // Gets discounted total
return total + 5; // Add shipping
}, { id: 'shipping', priority: 20 });
// Result: original() → discount patch → shipping patch
// So: (100 * 0.9) + 5 = 95

You can patch methods before the component exists:

// Works even if CartWidget isn't registered yet
patchMethod('CartWidget', 'computeTotal', applyDiscount, { id: 'discount' });
// When CartWidget registers, the patch is applied automatically
const { unpatchMethod } = window.fullfinity;
unpatchMethod('CartWidget', 'computeTotal', 'discount');
const { listMethodPatches } = window.fullfinity;
console.log(listMethodPatches('CartWidget', 'computeTotal'));
// ['discount', 'shipping']

Slots let components define extension points where addons can inject UI:

Use useSlot to get slot contents and automatically re-render when addons register:

const { registry, html, useSlot } = window.fullfinity;
registry.component('ProductCard', function(props) {
// useSlot re-renders when slot contents change
const afterPrice = useSlot('ProductCard:afterPrice');
const beforeTitle = useSlot('ProductCard:beforeTitle');
return html`
<div class="product-card">
${beforeTitle.map(C => html`<${C} ...${props} />`)}
<h3>${props.name}</h3>
<span class="price">${props.price}</span>
${afterPrice.map(C => html`<${C} ...${props} />`)}
</div>
`;
});

Note: Use useSlot() instead of registry.getSlot() to ensure the component re-renders when addons register into the slot after initial mount.

const { registry, html } = window.fullfinity;
// Simple slot registration
function LoyaltyPoints({ price }) {
const points = Math.floor(price / 10);
return html`<span class="loyalty-points">+${points} pts</span>`;
}
registry.slot('ProductCard:afterPrice', LoyaltyPoints);
// With options
registry.slot('ProductCard:afterPrice', DiscountBadge, {
priority: 5, // Lower runs first
id: 'discount-badge', // For removal
when: () => window.discountsEnabled
});
registry.removeSlot('ProductCard:afterPrice', 'discount-badge');
// All slots for a component
registry.listSlots('ProductCard');
// ['ProductCard:beforeTitle', 'ProductCard:afterPrice']
// All slots
registry.listSlots();
Use CaseUse SlotsUse Patches
Add UI to predefined points
Modify existing behavior
Multiple addons add to same spot
Wrap entire component
Intercept/modify props
Component defines extension points

If you patch a component that hasn’t been registered yet, the patch is automatically deferred:

// This works even if 'FutureComponent' doesn't exist yet
patch('FutureComponent', (Original) => function(props) {
// When FutureComponent is eventually registered,
// this patch will be applied automatically
return html`
<div class="enhanced">
<${Original} ...${props} />
</div>
`;
}, { id: 'enhancement' });

Multiple modules can patch the same component. Order is determined by:

  1. Priority (lower first)
  2. Dependencies (after constraints)
  3. Registration order (module load order)
// Module A (priority 10)
patch('Cart', addDiscountField, { id: 'discount', priority: 10 });
// Module B (priority 20, after discount)
patch('Cart', addDiscountValidation, {
id: 'discount-validation',
priority: 20,
after: 'discount'
});
// Module C (priority 15, but no dependency)
patch('Cart', addLoyaltyDisplay, { id: 'loyalty', priority: 15 });
// Execution order: discount → loyalty → discount-validation
patch('ProductTabs', (Original) => function(props) {
const { html } = window.fullfinity;
const extendedTabs = [
...(props.tabs || []),
{
id: 'reviews',
label: 'Reviews',
content: html`<${ReviewsPanel} productId=${props.productId} />`
}
];
return html`<${Original} ...${props} tabs=${extendedTabs} />`;
}, { id: 'reviews-tab' });
patch('ContactForm', (Original) => function(props) {
const { html } = window.fullfinity;
function handleSubmit(data) {
// Add tracking
analytics.track('contact_form_submit', data);
// Call original handler
props.onSubmit?.(data);
}
return html`<${Original} ...${props} onSubmit=${handleSubmit} />`;
}, { id: 'form-tracking' });
patch('CheckoutButton', (Original) => function(props) {
const { useState, useEffect, html } = window.fullfinity;
const [hasSubscription, setHasSubscription] = useState(false);
useEffect(() => {
fetch('/api/user/subscription')
.then(r => r.json())
.then(data => setHasSubscription(data.active));
}, []);
if (!hasSubscription) {
return html`
<div class="checkout-upsell">
<${Original} ...${props} />
<a href="/subscribe" class="upgrade-link">
Subscribe for free shipping!
</a>
</div>
`;
}
return html`<${Original} ...${props} />`;
}, { id: 'subscription-upsell' });
// Good: Has ID for debugging and removal
patch('Button', patcher, { id: 'my-module-button-patch' });
// Bad: No ID - can't unpatch or reference in 'after'
patch('Button', patcher);
// Good: Spread props to preserve all original functionality
patch('Button', (Original) => function(props) {
return html`<${Original} ...${props} />`;
}, { id: 'button-wrapper' });
// Bad: Only passing some props - onClick, className, etc. are lost!
patch('Button', (Original) => function({ label }) {
return html`<${Original} label=${label} />`;
}, { id: 'broken-button' });

If you expect multiple addons to add UI to the same spot, use slots instead of patches:

// Bad: Multiple patches fighting for same spot
patch('Header', addPromo, { id: 'promo' });
patch('Header', addAnnouncement, { id: 'announcement' });
// Good: Component defines slot, addons register into it
registry.slot('Header:announcements', PromoBanner);
registry.slot('Header:announcements', AnnouncementBar);
/**
* Patch: ProductCard + Loyalty Points
*
* Module: ecommerce_loyalty
* Patches: ecommerce.ProductCard
*
* Adds loyalty points badge and tracking to product cards.
* Points are calculated based on product price.
*/
patch('ProductCard', (Original) => function(props) {
// Implementation...
}, { id: 'loyalty-points', priority: 20 });
// In browser console
const { listPatches, registry } = window.fullfinity;
// See all patch IDs
console.log(listPatches('ProductCard'));
// Get original unpatched component
const Original = registry.getOriginal('ProductCard');
// Get current patched component
const Patched = registry.component('ProductCard');
patch('DebugComponent', (Original) => {
console.log('Patching DebugComponent');
console.log('Received Original:', Original);
return function(props) {
console.log('Rendering patched DebugComponent with props:', props);
return html`<${Original} ...${props} />`;
};
}, { id: 'debug-patch' });
ParameterTypeDescription
namestringComponent name to patch
patcherfunction(Original) => PatchedComponent
options.idstringUnique identifier
options.prioritynumberLower runs first (default: 10)
options.afterstring | string[]Run after these patch IDs
options.whenfunctionCondition function () => boolean

Remove a patch by ID. Component is rebuilt without it.

patchMethod(name, methodName, handler, options?)

Section titled “patchMethod(name, methodName, handler, options?)”
ParameterTypeDescription
namestringComponent name
methodNamestringMethod to patch
handlerfunction(_super, ...args) => result
options.prioritynumberLower runs first (default: 10)
options.idstringUnique identifier for removal

Remove a method patch by ID.

Returns array of patch IDs for the component.

Returns array of method patch IDs.

registry.slot(slotName, component, options?)

Section titled “registry.slot(slotName, component, options?)”
ParameterTypeDescription
slotNamestringSlot name (e.g., ‘ProductCard:afterPrice’)
componentfunctionComponent to inject
options.prioritynumberLower runs first (default: 10)
options.idstringUnique identifier for removal
options.whenfunctionCondition function

Returns array of components registered in the slot (filtered by conditions).

Remove a component from a slot by ID.

List slot names. If componentName provided, filters to slots starting with that prefix.

Get the original unpatched component.