# Wire Adapters
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.
# 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 if:true={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.)