LWC: A sample fetch data accessor


What would an LWC application be if it does not fetch data from a server? The browser now made this easy, particularly with the fetch function, or its polyfill for older browsers. The general pattern with LWC/Web Components is to fetch data in connectedCallback(), once the component is added to the DOM. If this can be achieved with simple code, let see how we can make it even easier with wire adapters. In particular, we like to do the following:
  • Have a simple syntax for calling the services.
  • Do not hard code the full URL in the components, so we can easily, and globally, switch the endpoint (dev, staging, prod...), or even mock the server for test purposes. 
  • Use URL variable parts, or query parameters.
  • Be able to re-fetch the data when needed, in particular when the variable values change.
The code that illustrates this post is available as part of the LWC essentials: https://github.com/LWC-Essentials/wire-adapters/tree/master/packages/fetch. Feel free to check it out and submit enhancements. or suggestions! You can also consume the NPM package @lwce/fetch.

Using the fetch wire adapter 

The syntax is straightforward:

@wire(useFetch, options) myproperty;

Let see an an example showing the wire adapter in action. This one calls a user service with query parameters:

export default class UserList extends LightningElement {
   @track queryParams = {
        offset: 0,
        limit: 10
    }
    @wire(useFetch, {
        url: '/users',
        queryParams: '$queryParams'
    }) users;


When the component is initialized, the fetch request is executed and the users variables get the result.
Requests can also be executed explicitly. Obviously, this is useful for update requests, but also for on demand GET requests:

export default class Users extends LightningElement {
    @track variables = {
        userId: 'xyz'
    }
    @wire(useFetch, {
        url: '/users/{userId}',
        lazy: true,
        variables: '$variables'
    }) userById;


    handleUserClick(event) {
        const idx = this._findUserIdx(event);
        if(idx!==undefined) {

            const userId = this.users.data.users[idx].email;
            this.userById.fetch( {variables:{userId} ).then( ()=>{
                alert(JSON.stringify(this.userById.data));
            });
        }
    }


The result property exposes a fetch() method that can be called programmatically, while the lazy parameter of the wire adapter prevent the automatic execution when the component is initialized.

There are several other options, but I don't want to go too much into the use of the adapter, as the README.md file already does that pretty extensively.

Let's rather dive into the implementation details.

Implementation details

The fetch wire adapter implementation is fairly straightforward, but there are a few traps that we should be aware of.

Duplicating the result object

The current result object is kept in a variable within the wire adapter closure, and new values are simply assigned to this object before it is sent to the component. This makes it easier from an implementation standpoint, for example setting the invariant values, like the fetchClient instance, or the fetch() method.
But has a drawback: the component will only detect a change, and thus redraw, when a new object is sent by the wire adapter. So the object kept by the adapter can't be sent as is to the component. Rather, the update() function duplicates it and send the copy to the component.

Concurrent requests

The result from a fetch() call is by definition retrieved asynchronously, which means that we do not control when that result will be available. Moreover, if multiple requests are sent sequentially by the same wire adapter, there is no guarantee that the respective results will come in the same order than the requests were emitted. Imagine a UI featuring a button that triggers a change in the wire adapter. If a user frenziedly clicks that button, it is likely that new requests will be emitted by the wire adapter before the results of the previous ones come back, with no guarantee that the results will be retrieved in order. This is well known issue in JavaScript based UI, generally hard to detect and debug.

Wire adapters, if not designed appropriately, are no exception. I created a code snippet to showcase the issue: https://developer.salesforce.com/docs/component-library/tools/playground/8peP8wto/12/edit.

Alright, now that we are aware of this issue, here is a set of potential solutions:
  1. Prevent a new request from being emitted while one is still pending 
  2. Ignore the oldest ones when their result comes back and only process the latest one
If 1. is easy to implement, it does not provide the best user experience because the UI is frozen until the current request's result comes back. On the other hand, it can also be implement at the application level, for example by disabling the button until the request comes back.
The fetch wire adapter implements the second one by default. It actually associates a unique index to each request, keeps it in the closure of response handler (fetchIndex), and also globally keeps the index of the latest request (fetchCurrent). When a result comes back, it only processes it if it corresponds to the latest request. All other results are simply ignored.

As a conclusion...

Basic wire adapters are easy to implement, while providing a unified way to expose data to component developers. Jump over the fence, and start to create yours! Or extend the one we are providing as part of the LWC-Essentials/wire-adapters project. I'd love to grow this repo with your contributions.


Comments