Why Reusability Is Hard
Every project starts with the intent to “build reusable components.” Most end up with components that are reused zero times — each feature forks a bespoke version that almost works.
The pattern isn’t laziness. It’s that designing for reuse means resisting the temptation to hard-code the current feature’s specifics into the component. You do extra work up front for a benefit that only materializes later. Most teams skip the up-front work and pay the cost slowly.
This article lays out the specific techniques that make LWC components actually reusable.
Design Principle 1: Clear Public API
A reusable component has an explicit, documented contract. Consumers should be able to use it without reading its source.
The public API is:
@apiproperties — inputs@apimethods — imperative controls- Custom events — outputs
- Slots — composition extension points
Everything else is implementation detail. If a consumer needs to reach into your internals, your API is incomplete.
@api Properties: Inputs
Every property a consumer must pass should be @api. Document types in JSDoc:
export default class DataTable extends LightningElement {
/** @type {Array<object>} Records to display */
@api records;
/** @type {Array<{field: string, label: string}>} Column definitions */
@api columns;
/** @type {number} Page size, default 10 */
@api pageSize = 10;
/** @type {boolean} Allow row selection */
@api selectable = false;
}
Default values go on the field. Required properties should throw (or show an error state) if missing — not silently render empty.
@api Methods: Imperative Controls
Methods a parent might need to invoke — refresh(), clearSelection(), focus() — should be @api public methods:
@api
refresh() {
this.loadData();
}
@api
clearSelection() {
this.selectedIds = [];
}
Parents access them via this.template.querySelector('c-data-table').refresh().
Don’t go overboard. Most parent-child communication should go through props and events. Reserve imperative methods for operations that genuinely can’t be expressed declaratively (focus management, animation triggers).
Custom Events: Outputs
When something happens inside the component that the parent cares about, dispatch a custom event.
const evt = new CustomEvent('rowselect', {
detail: { recordId: row.id, row }
});
this.dispatchEvent(evt);
Conventions:
- Event names lowercase with no hyphen (
rowselect, notrow-select). detailis an object with meaningful fields.- Don’t reuse generic names like
change,click— consumers will confuse them with platform events.
For cross-tree events, use bubbles: true, composed: true carefully — composed events leak through shadow DOM, which is sometimes what you want but often leads to surprising reactions.
Slots: Composition Extension
Slots let consumers inject markup into your component. Three types you’ll use:
Default slot:
<!-- your component -->
<div class="card">
<slot></slot>
</div>
<!-- consumer -->
<c-card>
<p>This goes in the default slot</p>
</c-card>
Named slots:
<!-- your component -->
<div class="card">
<header><slot name="header"></slot></header>
<main><slot></slot></main>
<footer><slot name="footer"></slot></footer>
</div>
<!-- consumer -->
<c-card>
<h2 slot="header">Title</h2>
<p>Body</p>
<div slot="footer">Actions</div>
</c-card>
Named slots let consumers compose rich layouts without you anticipating every combination.
Avoid slotting data. Slots are for markup. If the consumer needs to pass data, use a prop.
Pattern: Base + Specialization
Two components: c-data-table (generic) and c-case-table (Case-specific using c-data-table inside).
The base handles pagination, sorting, selection. The specialization configures columns, adds Case-specific actions, and dispatches domain-specific events.
<!-- c-case-table -->
<template>
<c-data-table
records={cases}
columns={caseColumns}
onrowselect={handleCaseSelect}>
</c-data-table>
</template>
This keeps the base generic (reusable for Lead tables, Opportunity tables) while allowing domain-specific wrappers.
Pattern: Render Mode via Prop
A component that displays differently in different contexts — compact vs. detailed, read-only vs. editable — should accept a mode prop:
@api mode = 'default'; // 'default' | 'compact' | 'detailed'
Template uses lwc:if to switch based on mode. Keeps one component usable across contexts without forking.
Cap the number of modes. If you’re on mode five, the component is probably doing too much.
Pattern: Design Token Theming
For visual variation (brand colors, spacing) without code forks, use Salesforce Design System CSS custom properties:
.card {
background: var(--slds-c-card-color-background, #fff);
border-radius: var(--slds-c-card-radius-border, 4px);
}
Consumers override tokens at their level, and the component picks them up.
Packaging for Actual Reuse
A component that lives in one repo used by one project isn’t actually reused. For multi-project reuse:
- Unlocked package per component family. Shared base package + per-project specialization packages.
- Git submodule or npm-style sharing of DX source (via a private artifact registry) for smaller teams.
- Managed package if the component library is a product.
Whichever you pick, have a versioning discipline — semver for the library, deliberate upgrade paths for consumers.
Documentation
A reusable component without documentation won’t be reused, because nobody trusts it. Minimum viable docs:
- README: what the component does in one paragraph.
- API reference: every
@apiproperty, method, and event with types and descriptions. - Example: one working example showing typical use.
Salesforce has a Storybook-like tool (LWC Component Explorer for internal orgs) that can generate component catalogs. Worth investing if your library has 10+ components.
Testing for Reuse
Unit tests for reusable components should cover:
- Every
@apiproperty, including defaults. - Every event, including detail shape.
- Public methods.
- Slot rendering with different content.
- Edge cases (empty arrays, null inputs).
Test coverage doubles as usage documentation. A thorough test file shows consumers every way the component can be used.
Anti-Patterns
Over-parameterization. A component with 20 props is probably two components welded together.
Smart components doing everything. Components that reach out to services, query Apex, and render UI are hard to reuse. Split “dumb” presentational components from “smart” containers.
Tight coupling to parent state. A component that only works inside a specific parent’s shape is not reusable. Keep the contract narrow.
Events without discipline. Components that fire 30 events, including fine-grained internal events, overwhelm consumers.
Frequently Asked Questions
When should I make a new component vs. add a prop?
If the behavior differs fundamentally, new component. If it’s a variation on the same behavior, prop. Rule of thumb: if you need more than 2 conditional branches based on a prop, you’re hiding a separate component.
Are there Salesforce-provided base components?
Yes — the lightning-* components (lightning-button, lightning-card, lightning-datatable). Use them as the base for domain-specific wrappers, don’t reimplement.
How do I expose a component to Lightning App Builder?
Create a .js-meta.xml file with <targets> including lightning__RecordPage, lightning__AppPage, etc. Expose @api properties as <targetConfig> properties for admin configuration.
Can I publish LWC to npm?
Not directly — LWC is Salesforce-specific. But Lightning Web Runtime (Hybrid) allows LWC to run off-platform, and that path supports npm publishing.