Skip to content

[labs/task] Task needs an arguments equality function. #3142

@justinfagnani

Description

@justinfagnani

Which package is this a feature request for?

Task (@lit-labs/task)

Description

The lack of an equality function to compare task arguments can cause some major DX problems.

Using an object or array literal from the args function will cause the task to continually fire, usually creating an iloop as the task completing causes another update cycle which checks for new arguments:

  _task = new Task<[Array<string>], Foo>(this, async ([strings]) => { ... }, () => ['a', 'b', 'c']);

To avoid this you have to hoist the array if you can, so that the === check works:

const strings = ['a', 'b', 'c'];
  _task = new Task<[Array<string>], Foo>(this, async ([strings]) => { ... }, () => string);

It's worse when you have an argument that's a deep object. This can be common if you're using Task to wrap an existing async API that takes such an object as part of its parameters.

Here's a controller that wraps the Google Maps Nearby Places API, which takes a PlaceSearchRequest request. We'd like to use Task to manage the API calls, so it's natural to ask for a PlaceSearchRequest from a user of the controller and pass that as a task arg.

import {ReactiveController, ReactiveControllerHost} from 'lit';
import {initialState, StatusRenderer, Task} from '@lit-labs/task';

export interface Options {
  getMap: () => google.maps.Map | undefined;
  getRequest: () => Partial<google.maps.places.PlaceSearchRequest> | undefined;
}

export class NearbySearchController implements ReactiveController {
  protected _host: ReactiveControllerHost;

  private _task: Task;

  constructor(host: ReactiveControllerHost, options: Options) {
    (this._host = host).addController(this);
    this._task = new Task<[
      google.maps.Map,
      google.maps.places.PlaceSearchRequest
    ], google.maps.places.PlaceResult[]>(
      host,
      async ([map, request]) => {
        if (request === undefined) {
          return initialState;
        }
        const service = new google.maps.places.PlacesService(map);
        return new Promise((res, rej) =>
          service.nearbySearch(request, (results, status) => {
            if (status === google.maps.places.PlacesServiceStatus.OK) {
              res(results);
            } else {
              rej(status);
            }
          })
        );
      },
      () => {
        const request = options.getRequest();
        return [options.getMap(), request] as [
          google.maps.Map,
          google.maps.places.PlaceSearchRequest
        ];
      }
    );
  }

  render(renderer: StatusRenderer<google.maps.places.PlaceResult>) {
    return this._task.render(renderer);
  }

  hostUpdate() {}
}
_nearbySearchController = new NearbySearchController(this, {
  getMap: () => this.map,
  getRequest: () => ({
    keyword: 'one two'
    location: this.location
  }),
});

This construction will cause the Task to run every update as well. The only solution is to destructure the PlaceSearchRequest object into literals and many arguments before passing to Task.

I would propose three built-in equality checks:

  • Strict (default)
  • Shallow
  • Deep

Alternatives and Workarounds

Destructuring args into primitives, hoisting const args.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    ✅ Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions