# Wire Adapters
Note
Building a custom wire adapter is not supported on the Salesforce Platform. See LWC Open Source.
LWC has an elegant way to provide a stream of data to a component. Define a data provider called a wire adapter. A wire adapter simply provides data. A wire adapter doesn't know anything about the components that it provides data to.
In a component, declare its data needs by using the @wire
decorator to connect (or wire) it to a wire adapter. In this example, the component is wired to the getBook
wire adapter, which we can assume provides details about a specific book. This declarative technique makes component code easy to read and reason about.
import { LightningElement } from 'lwc'; export default class WireExample extends { @api bookId; @wire(getBook, { id: '$bookId'}) book; }
Wire adapters are part of LWC's reactivity system. An @wire
takes the name of a wire adapter and an optional configuration object. You can use a $
to mark the property of a configuration object as dynamic. When a dynamic property’s value changes, the wire adapter's update
method executes with the new value. When the wire adapter provisions new data, the component rerenders.
Another reason to use wire adapters is that they're a statically analyzable expression of data. Static analysis tools find issues before you run code and can increase code quality and security.
Note
The @wire
delegates control flow to the Lightning Web Components engine. Delegating control is great for read operations, but it isn’t great for create, update, and delete operations. As a developer, you want complete control over operations that change data.
# Syntax to Implement a Wire Adapter
A wire adapter is a class that implements the WireAdapter
interface, and is a named or default export from an ES6 module.
When a component is constructed, if it has an @wire
to a wire adapter class, an instance of the wire adapter class is constructed. The wire adapter invokes the DataCallback
function to provision data to the component (into the wired field or function).
If the @wire
configuration object changes, the wire adapter's update(newConfigValues)
is called. The wire adapter fetches the necessary data then calls dataCallback(newValueToProvision)
to provision the value to the component.
interface WireAdapter { update(config: ConfigValue); connect(); disconnect(); } interface WireAdapterConstructor { new (callback: DataCallback): WireAdapter; } type DataCallback = (value: any) => void; type ConfigValue = Record<String, any>;
update(config: ConfigValue)
—Invoked the first time the component is created and whenever the wire adapter's configuration object changes. The new configuration object is passed toupdate()
. You can't rely on the order in whichupdate
andconnect
are called; the order can be different even per component instance.connect()
—Invoked when the component connects to the DOM.disconnect()
—Invoked when the component disconnects from the DOM.
The wire adapter's code shouldn’t be aware of the components to which it provides data. The wire adapter simply implements this interface to produce a stream of data.
# Syntax to Consume a Wire Adapter
To consume a wire adapter, decorate a field or function with @wire
. The @wire
takes a wire adapter and optionally a configuration object. The data is provisioned into the wired field or function.
import { LightningElement } from 'lwc'; import { adapterId } from 'adapterModule'; export default class WireExample extends LightningElement { @wire(adapterId[, adapterConfig]) fieldOrFunction; }
adapterId
(WireAdapter)—The identifier of the wire adapter.adapterModule
(String)—The identifier of the module that contains the wire adapter, in the formatnamespace/moduleName
.adapterConfig
(Object | undefined)—Optional. A configuration object specific to the wire adapter.
A configuration object can reference a property of the component instance that's declared as a class field. In the configuration object, prefix the property with $
, which tells LWC to evaluate it as this.propertyName
. The property is now dynamic; if its value changes, the update
method of the adapter executes. When new data is provisioned, the component rerenders. Use the $
prefix for top-level values in the configuration object. Nesting the $
prefix, such as in an array like ['$myIds']
, makes it a literal string.
Don’t update a configuration object property in renderedCallback()
as it can result in an infinite loop.
fieldOrFunction
—A field or function that receives the stream of data.
Important
Objects passed to a component are read-only. To mutate the data, a component should make a shallow copy of the objects it wants to mutate. It’s important to understand this concept when working with data. See Data Flow Considerations.
# Context Providers
Note
Context providers are not supported on the Salesforce Platform. See LWC Open Source.
Context providers allow contextual state to be shared among all components in a section of the DOM tree without the need for "prop-drilling" (i.e. passing the same prop through multiple layers of components). This is similar to useContext
in React or simply using DOM events to communicate between ancestors and descendants.
To create a context provider, use the createContextProvider
API:
import { createContextProvider } from 'lwc'; const contextualizer = createContextProvider(WireAdapter);
Passing a WireAdapter
(as described above) into createContextProvider
results in a contextualizer. This contextualizer can then be invoked in the connectedCallback
of the provider component:
export default class ProviderComponent extends LightningElement { connectedCallback() { contextualizer(this, { consumerConnectedCallback(consumer) { consumer.provide({ value: 'contextual value' }); } }); } }
Note the consumerConnectedCallback
above, which is fired when a context consumer connects to the DOM.
This provider component should contain the consumer component somewhere in its descendant tree:
<!-- providerComponent.html --> <template> <x-consumer-component></x-consumer-component> </template>
The consumer component can then receive the value
from the context provider via @wire
:
export default class ConsumerComponent extends LightningElement { @wire(WireAdapter) value; }
This WireAdapter
should, at a minimum, be defined like so:
class WireAdapter { static contextSchema = { value: 'required' // or 'optional' }; constructor(callback) { this._callback = callback; } connect() { /* noop */ } disconnect() { /* noop */ } update(_config, context) { this._callback(context.value); } }
The contextSchema
above describes which values are expected to be supplied in the context (e.g. value
in this example). These values are recommended to be marked either 'optional'
or 'required'
. (These are purely for documentation purposes; they do not affect the runtime behavior.)
The connect
and disconnect
callbacks above will be invoked when the consumer component connects or disconnects from the DOM. Whereas update
will be called whenever the provider calls consumer.provide()
.
The _config
above is optional, and corresponds to any configuration options passed in to the @wire
by the consumer, like so:
export default class ConsumerComponent extends LightningElement { @wire(WireAdapter, { some: 'config' }) value; }
In this example, the _config
would be the object { some: 'config' }
.
If no config is provided to @wire
(as in the prior example), then the _config
passed to the WireAdapter
above will be an empty object ({}
).
Putting it all together, the value
property should be populated with the string "contextual value"
, as shown in the following example:
# Example: RCast App
The RCast app is a PWA podcast player written with Lightning Web Components.
https://github.com/pmdartus/rcast
RCast uses a wire adapter called connectStore
to provision data to its components.
export class connectStore { dataCallback; store; subscription; connected = false; constructor(dataCallback) { this.dataCallback = dataCallback; } connect() { this.connected = true; this.subscribeToStore(); } disconnect() { this.unsubscribeFromStore(); this.connected = false; } update(config) { this.unsubscribeFromStore(); this.store = config.store; this.subscribeToStore(); } subscribeToStore() { if (this.connected && this.store) { const notifyStateChange = () => { const state = this.store.getState(); this.dataCallback(state); }; this.subscription = this.store.subscribe(notifyStateChange); notifyStateChange(); } } unsubscribeFromStore() { if (this.subscription) { this.subscription(); this.subscription = undefined; } } }
# Example: Book List App
This simple app is an editable list of books. You can create, edit, and delete book titles.
<!-- app.html --> <template> <c-book-create onbookcreate={handleCreateBook}></c-book-create> <c-book-list oneditbook={handleEditBook} ondeletebook={handleDeleteBook} ></c-book-list> <template lwc:if={inEditMode}> <c-book-edit book-id={editBookId} onsaveedit={handleSaveEdition} oncanceledit={handleCancelEdition} ></c-book-edit> </template> </template>
When you click to edit a book title, an input field appears with Save and Cancel buttons.
<!-- bookEdit.html --> <template> <input type="hidden" name="id" value={bookId} /> <input name="title" value={draftTitle} onchange={handleTitleChange} /> <button onclick={handleSave}>Save</button> <button onclick={handleCancel}>Cancel</button> </template>
When the book-edit
component is constructed, the @wire
provisions the data from the getBook
wire adapter. Because the adapter config $bookId
is prefixed with $
, when its value changes, the @wire
provisions new data and the component rerenders.
// bookEdit.js import { LightningElement, api, wire } from 'lwc'; import { getBook } from 'c/bookApi'; export default class BookEdit extends LightningElement { @api bookId; draftTitle = ""; @wire(getBook, { id: '$bookId'}) bookDetails(book) { if (book === null) { console.error("Book with id %s does not exist", this.bookId); } this.draftTitle = book.title; }; handleTitleChange(event) { this.draftTitle = event.target.value; } handleSave() { this.dispatchEvent( new CustomEvent('saveedit', { detail: { id: this.bookId, title: this.draftTitle } }) ); } handleCancel() { this.draftTitle = ""; this.dispatchEvent( new CustomEvent('canceledit') ); } }
The wire adapters for this app are defined in bookApi.js
.
// bookApi.js import { bookEndpoint } from './server'; const getBooksInstances = new Set(); function refreshGetBooksInstances() { getBooksInstances.forEach(instance => instance._refresh()); } export class getBooks { constructor(dataCallback) { this.dataCallback = dataCallback; this.dataCallback(); } connect() { getBooksInstances.add(this); } disconnect() { getBooksInstances.remove(this); } update() { this._refresh(); } _refresh() { const allBooks = bookEndpoint.getAll(); this.dataCallback(allBooks); }; } export class getBook { connected = false; bookId; constructor(dataCallback) { this.dataCallback = dataCallback; } connect() { this.connected = true; this.provideBookWithId(this.bookId); } disconnect() { this.connected = false; } update(config) { if (this.bookId !== config.id) { this.bookId = config.id; this.provideBookWithId(this.bookId); } } providBookWithId(id) { if (this.connected && this.bookId !== undefined) { const book = bookEndpoint.getById(id); if (book) { this.dataCallback(Object.assign({}, book)); } else { this.dataCallback(null); } } } } export function createBook(title) { bookEndpoint.create(title); refreshGetBooksInstances(); } export function deleteBook(id) { bookEndpoint.remove(id); refreshGetBooksInstances(); } export function updateBook(id, newTitle) { bookEndpoint.update(id, newTitle); refreshGetBooksInstances(); }
This app also includes an abstraction of server code.
// server.js // A server abstraction class BookEndpoint { bookStore = new Map(); nextBookId = 0; getAll() { return this.bookStore.values() } getById(id) { return this.bookStore.get(parseInt(id)); } create(title) { const book = { id: this.nextBookId++, title, }; this.bookStore.set(book.id, book); } update(id, title) { const book = { id: parseInt(id), title }; this.bookStore.set(book.id, book); } remove(id) { this.bookStore.delete(parseInt(id)); } } export const bookEndpoint = new BookEndpoint(); bookEndpoint.create('The Way of Kings');
(The component Book List app also includes book-list
and book-create
components. These components use the same techniques, so we've omitted them for space.)