Vaadin grid re-requesting the same data from the data provider at startup

Hi Team,

I was wondering if you’d be able to assist, I’m using the Lit-element version of the grid.

When the grid loads it is is calling the dataprovider over a dozen times for the same page with the same params, am I doing anything dumb here?

const username = 'Username';
const status = 'Status';
const components = 'Components';
const details = 'Details';
const id = 'Id';

@customElement('page-monitor-requests')
export class PageMonitorRequests extends LitElement {
  @query('#grid') grid: Grid | undefined;

  @state() loading = true;
  @state() searching = false;
  @state() noResults = false;

  @state() userFilter: string = '';
  @state() statusFilter: string = '';
  @state() componentsFilter: string = '';
  @state() idFilter: string = '';
  @state() detailsFilter: string = '';

  static get styles() {
    return css`
      vaadin-grid#grid {
        overflow: hidden;
        height: calc(100vh - 56px);
        --divider-color: rgb(223, 232, 239);
      }

      vaadin-grid#grid {
        overflow: hidden;
      }

      vaadin-text-field {
        padding: 0px;
        margin: 0px;
      }

      vaadin-grid-cell-content {
        padding-top: 0px;
        padding-bottom: 0px;
        margin: 0px;
      }

      .overlay {
        width: 100%;
        height: 100%;
        position: fixed;
      }

      .overlay__inner {
        width: 100%;
        height: 100%;
        position: absolute;
      }

      .overlay__content {
        left: 20%;
        position: absolute;
        top: 20%;
        transform: translate(-50%, -50%);
      }

      .spinner {
        width: 75px;
        height: 75px;
        display: inline-block;
        border-width: 2px;
        border-color: rgba(255, 255, 255, 0.05);
        border-top-color: cornflowerblue;
        animation: spin 1s infinite linear;
        border-radius: 100%;
        border-style: solid;
      }

      @keyframes spin {
        100% {
          transform: rotate(360deg);
        }
      }

      .cover {
        object-fit: cover;
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
      }
    `;
  }

  render() {
    return html`
      <div
        class="overlay"
        style="z-index: 2"
        ?hidden="${!(this.loading || this.searching)}"
      >
        <div class="overlay__inner">
          <div class="overlay__content">
            <span class="spinner"></span>
          </div>
        </div>
      </div>

      <vaadin-grid
        id="grid"
        column-reordering-allowed
        multi-sort
        theme="compact row-stripes no-row-borders no-border"
        .dataProvider="${(
          params: GridDataProviderParams<DeploymentRequestApiModel>,
          callback: GridDataProviderCallback<DeploymentRequestApiModel>
        ) => {
          if (params.sortOrders.length !== 1) {
            return;
          }

          if (this.detailsFilter !== '' && this.detailsFilter !== undefined) {
            params.filters.push({ path: 'Project', value: this.detailsFilter });
            params.filters.push({
              path: 'EnvironmentName',
              value: this.detailsFilter
            });
            params.filters.push({
              path: 'BuildNumber',
              value: this.detailsFilter
            });
          }

          if (this.idFilter !== '' && this.idFilter !== undefined) {
            params.filters.push({ path: 'Id', value: this.idFilter });
          }

          if (this.userFilter !== '' && this.userFilter !== undefined) {
            params.filters.push({ path: 'UserName', value: this.userFilter });
          }

          if (this.statusFilter !== '' && this.statusFilter !== undefined) {
            params.filters.push({ path: 'Status', value: this.statusFilter });
          }

          if (
            this.componentsFilter !== '' &&
            this.componentsFilter !== undefined
          ) {
            params.filters.push({
              path: 'Components',
              value: this.componentsFilter
            });
          }
          
          console.log('Requesting page:', params.page + 1, 'with filters:', params.filters, 'and sort orders:', params.sortOrders);

          const api = new RequestStatusesApi();
          api
            .requestStatusesPut({
              pagedDataOperators: {
                Filters: params.filters.map(
                  (f: GridFilterDefinition): PagedDataFilter => ({
                    Path: f.path,
                    FilterValue: f.value
                  })
                ),
                SortOrders: params.sortOrders.map(
                  (s: GridSorterDefinition): PagedDataSorting => ({
                    Path: s.path,
                    Direction: s.direction?.toString()
                  })
                )
              },
              limit: params.pageSize,
              page: params.page + 1
            })
            .subscribe({
              next: (data: GetRequestStatusesListResponseDto) => {
                data.Items?.map(
                  item => (item.UserName = item.UserName?.split('\\')[1])
                );
                console.log('Received data:', data);
                callback(data.Items ?? [], data.TotalItems);
                this.dispatchEvent(
                  new CustomEvent('searching-requests-finished', {
                    detail: data,
                    bubbles: true,
                    composed: true
                  })
                );
              },
              error: (err: any) => {
                const notification = new ErrorNotification();
                notification.setAttribute(
                  'errorMessage',
                  err.response.ExceptionMessage
                );
                this.shadowRoot?.appendChild(notification);
                notification.open();
                console.error(err);
                callback([], 0);
                this.dispatchEvent(
                  new CustomEvent('searching-requests-finished', {
                    detail: { TotalItems: 0 },
                    bubbles: true,
                    composed: true
                  })
                );
              },
              complete: () => {
                this.dispatchEvent(
                  new CustomEvent('monitor-requests-loaded', {
                    detail: {},
                    bubbles: true,
                    composed: true
                  })
                );

                console.log(
                  `done loading request Statuses page:${params.page + Number(1)}`
                );
              }
            });
        }}"
        ?hidden="${this.loading}"
        style="z-index: 1"
      >
        <vaadin-grid-column
          path="Id"
          resizable
          auto-width
          .headerRenderer="${this.idHeaderRenderer}"
          .renderer="${this.idRenderer}"
        ></vaadin-grid-column>
        <vaadin-grid-column
          header="Details"
          resizable
          auto-width
          .headerRenderer="${this.detailsHeaderRenderer}"
          .renderer="${this.detailsRenderer}"
        >
        </vaadin-grid-column>
        <vaadin-grid-column
          resizable
          .renderer="${this.timingsRenderer}"
          header="Timings"
          auto-width
        ></vaadin-grid-column>
        <vaadin-grid-column
          path="UserName"
          header="User"
          .headerRenderer="${this.usersHeaderRenderer}"
          resizable
          auto-width
        >
        </vaadin-grid-column>
        <vaadin-grid-column
          path="Status"
          header="Status"
          .headerRenderer="${this.statusHeaderRenderer}"
          resizable
          auto-width
        >
        </vaadin-grid-column>
        <vaadin-grid-column
          .renderer="${this._requestControlsRenderer}"
          resizable
          width="100px"
        >
        </vaadin-grid-column>
        <vaadin-grid-column
          path="Components"
          header="Components"
          .headerRenderer="${this.componentsHeaderRenderer}"
          resizable
          auto-width
        >
        </vaadin-grid-column>
      </vaadin-grid>
      <img
        class="cover"
        style="z-index: 2; height: 400px"
        ?hidden="${!this.noResults}"
        src="/hegsie_white_background_cartoon_geek_code_simple_icon_searching_12343b57-9c4e-45c6-b2f3-7765e8596718.png"
        alt="No Results Found"
      />
    `;
  }

  protected firstUpdated(
    _changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
  ): void {
    super.firstUpdated(_changedProperties);

    this.addEventListener(
      'request-cancelled',
      this.requestCancelled as EventListener
    );
    this.addEventListener(
      'request-restarted',
      this.requestRestarted as EventListener
    );
    this.addEventListener('refresh-requests', this.updateGrid as EventListener);

    this.addEventListener(
      'monitor-requests-loaded',
      this.monitorRequestsLoaded as EventListener
    );

    this.addEventListener(
      'searching-requests-started',
      this.searchingRequestsStarted as EventListener
    );

    this.addEventListener(
      'searching-requests-finished',
      this.searchingRequestsFinished as EventListener
    );
  }

  private searchingRequestsStarted(event: CustomEvent) {
    this.searching = true;
    if (event.detail.value !== undefined) {
      this.debouncedInputHandler(event.detail.field, event.detail.value);
      this.grid?.clearCache();
    }
  }

  private debouncedInputHandler = this.debounce(
    (field: string, value: string) => {
      switch (field) {
        case status:
          this.statusFilter = value;
          break;
        case username:
          this.userFilter = value;
          break;
        case components:
          this.componentsFilter = value;
          break;
        case id:
          this.idFilter = value;
          break;
        case details:
          this.detailsFilter = value;
          break;
        default:
          break;
      }
      console.log('Debounced Value:', value); // Perform logic (e.g., search or filtering)
    },
    500
  );

  private searchingRequestsFinished(e: CustomEvent) {
    const data: GetRequestStatusesListResponseDto = e.detail;
    if (data.TotalItems === 0) this.noResults = true;
    else this.noResults = false;

    this.searching = false;
  }

  private monitorRequestsLoaded() {
    this.loading = false;
  }

  updateGrid() {
    if (this.grid) {
      this.grid.clearCache();
      this.loading = true;
    }
  }

  requestCancelled(e: CustomEvent) {
    this.updateGrid();
    Notification.show(`Cancelled request with ID: ${e.detail.requestId}`, {
      theme: 'success',
      position: 'bottom-start',
      duration: 5000
    });
  }

  requestRestarted(e: CustomEvent) {
    this.updateGrid();
    Notification.show(`Restarted request with ID: ${e.detail.requestId}`, {
      theme: 'success',
      position: 'bottom-start',
      duration: 5000
    });
  }

  private detailsRenderer = (
    root: HTMLElement,
    _: HTMLElement,
    model: GridItemModel<DeploymentRequestApiModel>
  ) => {
    const request = model.item;
    render(
      html`
        <vaadin-horizontal-layout style="align-items: center;" theme="spacing">
          <vaadin-vertical-layout>
            <div>${request.Project} - ${request.EnvironmentName}</div>
            <div
              style="font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color);"
            >
              ${request.BuildNumber}
            </div>
          </vaadin-vertical-layout>
        </vaadin-horizontal-layout>
      `,
      root
    );
  };

  private timingsRenderer = (
    root: HTMLElement,
    _: HTMLElement,
    model: GridItemModel<DeploymentRequestApiModel>
  ) => {
    const request = model.item as DeploymentRequestApiModel;
    let sTime = '';
    let sDate = '';
    let cTime = '';
    let cDate = '';

    if (request.StartedTime !== undefined && request.StartedTime !== null) {
      sTime = new Date(request.StartedTime ?? '')?.toLocaleTimeString('en-GB');
      sDate = new Date(request.StartedTime ?? '')?.toLocaleDateString('en-GB', {
        day: '2-digit',
        month: '2-digit',
        year: 'numeric'
      });
    }
    if (request.CompletedTime !== undefined && request.CompletedTime !== null) {
      cTime = new Date(request.CompletedTime ?? '')?.toLocaleTimeString(
        'en-GB'
      );
      cDate = new Date(request.CompletedTime ?? '')?.toLocaleDateString(
        'en-GB',
        {
          day: '2-digit',
          month: '2-digit',
          year: 'numeric'
        }
      );
    }

    render(
      html`
        <vaadin-horizontal-layout style="align-items: center;" theme="spacing">
          <vaadin-vertical-layout
            style="line-height: var(--lumo-line-height-s);"
          >
            <div>${`${sDate} ${sTime}`}</div>
            <div>${`${cDate} ${cTime}`}</div>
          </vaadin-vertical-layout>
        </vaadin-horizontal-layout>
      `,
      root
    );
  };

  private idRenderer = (
    root: HTMLElement,
    _: HTMLElement,
    model: GridItemModel<DeploymentRequestApiModel>
  ) => {
    const request = model.item;
    render(
      html`
        <vaadin-horizontal-layout style="align-items: center;" theme="spacing">
          <span> ${request.Id} </span>
          <vaadin-button
            title="View Detailed Results"
            theme="icon"
            @click="${() => {
              const event = new CustomEvent('open-monitor-result', {
                detail: {
                  request,
                  message: 'Show results for Request'
                },
                bubbles: true,
                composed: true
              });
              this.dispatchEvent(event);
            }}"
          >
            <vaadin-icon
              icon="vaadin:ellipsis-dots-h"
              style="color: cornflowerblue"
            ></vaadin-icon>
          </vaadin-button>
        </vaadin-horizontal-layout>
      `,
      root
    );
  };

  _requestControlsRenderer(
    root: HTMLElement,
    _: HTMLElement,
    { item }: GridItemModel<DeploymentRequestApiModel>
  ) {
    render(
      html` <request-controls
        .requestId="${item.Id ?? 0}"
        .cancelable="${item.UserEditable &&
        (item.Status === 'Running' ||
          item.Status === 'Requesting' ||
          item.Status === 'Pending' ||
          item.Status === 'Restarting')}"
        .canRestart="${item.UserEditable && item.Status !== 'Pending'}"
      ></request-controls>`,
      root
    );
  }

  idHeaderRenderer(root: HTMLElement) {
    render(
      html`
        <vaadin-button
          theme="icon small"
          style="padding: 0px; margin: 0px"
          @click="${() => {
            const event = new CustomEvent('refresh-requests', {
              detail: {},
              bubbles: true,
              composed: true
            });
            this.dispatchEvent(event);
          }}"
        >
          <vaadin-icon
            icon="icons:refresh"
            style="color: cornflowerblue"
          ></vaadin-icon>
        </vaadin-button>
        <vaadin-grid-sorter
          path="Id"
          direction="desc"
          style="align-items: normal"
        ></vaadin-grid-sorter>
        <vaadin-text-field
          placeholder="Id"
          clear-button-visible
          focus-target
          style="width: 100px"
          theme="small"
          @value-changed="${(e: CustomEvent) => {
            this.dispatchEvent(
              new CustomEvent('searching-requests-started', {
                detail: {
                  field: id,
                  value: e.detail.value
                },
                bubbles: true,
                composed: true
              })
            );
          }}"
        ></vaadin-text-field>
      `,
      root
    );
  }

  detailsHeaderRenderer(root: HTMLElement) {
    render(
      html`
        <vaadin-text-field
          placeholder="Details"
          clear-button-visible
          focus-target
          style="width: 110px"
          theme="small"
          @value-changed="${(e: CustomEvent) => {
            this.dispatchEvent(
              new CustomEvent('searching-requests-started', {
                detail: {
                  field: details,
                  value: e.detail.value
                },
                bubbles: true,
                composed: true
              })
            );
          }}"
        ></vaadin-text-field>
      `,
      root
    );
  }

  usersHeaderRenderer(root: HTMLElement) {
    render(
      html`
        <vaadin-text-field
          placeholder="Username"
          clear-button-visible
          focus-target
          style="width: 100px"
          theme="small"
          @value-changed="${(e: CustomEvent) => {
            this.dispatchEvent(
              new CustomEvent('searching-requests-started', {
                detail: {
                  field: username,
                  value: e.detail.value
                },
                bubbles: true,
                composed: true
              })
            );
          }}"
        ></vaadin-text-field>
      `,
      root
    );
  }

  statusHeaderRenderer(root: HTMLElement) {
    render(
      html`
        <vaadin-text-field
          placeholder="Status"
          clear-button-visible
          focus-target
          style="width: 100px"
          theme="small"
          @value-changed="${(e: CustomEvent) => {
            this.dispatchEvent(
              new CustomEvent('searching-requests-started', {
                detail: {
                  field: status,
                  value: e.detail.value
                },
                bubbles: true,
                composed: true
              })
            );
          }}"
        ></vaadin-text-field>
      `,
      root
    );
  }

  componentsHeaderRenderer(root: HTMLElement) {
    render(
      html`
        <vaadin-text-field
          placeholder="Components"
          clear-button-visible
          focus-target
          style="width: 110px"
          theme="small"
          @value-changed="${(e: CustomEvent) => {
            this.dispatchEvent(
              new CustomEvent('searching-requests-started', {
                detail: {
                  field: components,
                  value: e.detail.value
                },
                bubbles: true,
                composed: true
              })
            );
          }}"
        ></vaadin-text-field>
      `,
      root
    );
  }

  // Utility function for debouncing
  private debounce(callback: (...args: any[]) => void, delay: number) {
    let timeout: number | undefined;
    return (...args: any[]) => {
      clearTimeout(timeout);
      timeout = window.setTimeout(() => callback(...args), delay);
    };
  }
}

All Vaadin fields dispatch a value-changed event for their initial value. With that it seems you are calling grid.clearCache a number of times when the view renders initially. Try removing some of the filters and see if that reduces the number of calls.

In general you might want to use the input or change events on Vaadin fields instead, as those only fire when the user changes the value.

Thankyou that definitely helped, now its down to only being called 3 times on start up after changing those valued-changed events to input events