LWC Mixins: Sharing Component Logic Without Copy-Paste
A practical pattern for reusing class-level logic across Lightning Web Components.
A mixin is useful when several LWC components need the same component behavior.
In LWC, this can be useful for things like navigation helpers, toast helpers, loading state, modal state, validation, shared wires, computed getters, or record-context checks.
What a mixin is
A mixin is a function that receives a base class and returns a new class that extends it.
export function SomeMixin(Base) {
return class extends Base {
// shared behavior
};
}
In LWC, the base class is usually LightningElement.
export default class MyComponent extends SomeMixin(LightningElement) {}
The component still has its own template and its own responsibility.
The mixin only adds reusable behavior.
A familiar example
Salesforce uses the same idea with NavigationMixin.
import { LightningElement } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
export default class MyComponent extends NavigationMixin(LightningElement) {
navigateToRecord(recordId) {
this[NavigationMixin.Navigate]({
type: 'standard__recordPage',
attributes: {
recordId,
actionName: 'view'
}
});
}
}
The component is still a normal LWC.
The mixin adds navigation behavior.
Use case
In this example, several components need the same context derived from a record.
A component on a record page can receive recordId as a public property.
import { api, LightningElement } from 'lwc';
export default class RecordDetails extends LightningElement {
@api recordId;
}
That part does not need a mixin.
The mixin becomes useful when multiple components use that recordId to load the same data and expose the same derived values.
For example:
this.recordStatus
this.parentRecordId
this.isSpecialFlow
Without a shared abstraction, every component needs the same wire and the same getters.
That is copy-paste.
And copy-paste eventually drifts.
Example implementation
// contextMixin.js
import { wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import RECORD_TYPE_FIELD from '@salesforce/schema/CustomObject__c.RecordType.DeveloperName';
import STATUS_FIELD from '@salesforce/schema/CustomObject__c.Status__c';
import PARENT_RECORD_FIELD from '@salesforce/schema/CustomObject__c.ParentRecord__c';
const RECORD_TYPES = {
SPECIAL_RECORD: 'Special_Record'
};
const FIELDS = [
RECORD_TYPE_FIELD,
STATUS_FIELD,
PARENT_RECORD_FIELD
];
export function SharedContextMixin(Base) {
return class extends Base {
@wire(getRecord, {
recordId: '$recordId',
fields: FIELDS
})
wiredRecord;
get recordTypeDeveloperName() {
return getFieldValue(this.wiredRecord?.data, RECORD_TYPE_FIELD);
}
get recordStatus() {
return getFieldValue(this.wiredRecord?.data, STATUS_FIELD);
}
get parentRecordId() {
return getFieldValue(this.wiredRecord?.data, PARENT_RECORD_FIELD);
}
get isSpecialFlow() {
return this.recordTypeDeveloperName === RECORD_TYPES.SPECIAL_RECORD;
}
};
}
The mixin expects the consuming component to provide recordId.
It does not need to declare it itself.
Consuming the mixin
import { api, LightningElement } from 'lwc';
import { SharedContextMixin } from 'c/mixins';
export default class RecordDetails extends SharedContextMixin(LightningElement) {
@api recordId;
get processedSections() {
if (!this.isSpecialFlow) {
// default logic
}
// special-flow logic
}
}
The component can now use the shared getters from the mixin:
this.recordTypeDeveloperName
this.recordStatus
this.parentRecordId
this.isSpecialFlow
The component does not care how those values are loaded.
That logic is centralized.
Re-exporting mixins
I prefer to keep one entry file for mixins:
// mixins.js
export * from './contextMixin';
Then components can import from a single module:
import { SharedContextMixin } from 'c/mixins';
The metadata file can stay private:
<?xml version="1.0" encoding="UTF-8" ?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>63.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
What mixins are good for
A mixin makes sense when the same component behavior is needed in more than one place.
Good candidates:
- navigation helpers
- toast helpers
- modal state
- loading state
- form validation
- shared wire logic
- shared computed getters
- record-context checks
- lifecycle setup
The important part is that the logic belongs on the component instance.
If it needs component state, lifecycle hooks, decorators, wires, or this, a mixin can make sense.
When not to use a mixin
Do not use a mixin just because code can be shared.
If the shared code is a pure function, use a normal JavaScript utility module.
export function formatStatus(status) {
return status?.toLowerCase();
}
That does not need a mixin.
If the logic belongs on the server, use Apex.
If only one component needs it, keep it in that component.
Premature abstraction is still abstraction debt.
Notes
Keep the mixin focused.
A SharedContextMixin should provide shared context.
A ToastMixin should provide toast helpers.
A LoadingStateMixin should provide loading state.
It should not slowly become a random collection of helpers, formatting methods, and unrelated business rules.
That is how abstractions become harder to understand than the copy-paste they replaced.
Final thought
Mixins are not only for record context.
They are for shared component behavior.
Use them when multiple LWC components need the same class-level logic, but should still remain separate components.
Keep them small, and focused.
References
-
Salesforce LWC Developer Guide: Component Record Context
Shows howrecordIdis provided to LWC components on record pages. -
Salesforce LWC Developer Guide: NavigationMixin
Official LWC example of a mixin adding reusable component behavior. -
MDN:
extendsand mix-ins
Explains the JavaScript class pattern where a function takes a superclass and returns a subclass.