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>;

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;
}

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.

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.)