lit-elementlit

In LitElement, how do I refresh external data in a child-component?


I have a child-component which takes a property (group). The child-component uses this property as an argument in a URL for fetching external data which it then renders as a list. The problem is, the child-component completes the render before fetch returns, using stale data from the previous fetch.

foods.js the parent component,

import {LitElement, html} from "https://cdn.jsdelivr.net/gh/lit/dist@2/all/lit-all.min.js";

import "./summary.js"

export class MyFoods extends LitElement {
    _dropdown(event) {
        if (event === undefined) {
            return
        }

        this.food = event.target.value
        console.log("_dropdown: " + this.food)
        this.requestUpdate()
    }

    constructor() {
        super()
        this.food = "all"
    }

    render() {
        return html`
        <select @click="${this._dropdown}">
            <option value="all">all</option>
            <option value="fruit">fruit</option>
            <option value="veg">veg</option>
        </select>
        <my-summary .group=${this.food}></my-summary>
        `
    }
}

customElements.define("my-foods", MyFoods)

summary.js the child component,

import {LitElement, html} from "https://cdn.jsdelivr.net/gh/lit/dist@2/all/lit-all.min.js";


export class MySummary extends LitElement {
    static properties = {
        group: {
            type: String
        }
    }

    _loadSummary() {
        let url = "/" + this.group
        console.log("url: " + url)

        let that = this

        fetch(url, {
            "credentials": "same-origin"
        })
        .then((resp) => resp.json())
        .then(function(data) {
            console.log(data)
            that._rows = []

            data.forEach(r => that._rows.push({"value": r.value}))
            // that.requestUpdate()
        })
    }

    constructor() {
        super()
        this.group = "all"
        this._rows = []

        console.log(this.group)
        // this._loadSummary()
    }

    render() {
        console.log("render: " + this.group)
        this._loadSummary()
        return html`
        <p>${this.group}</p>
        <ul>
            ${[...this._rows].map(r => html`<li>${r.value}</li>`)}
        </ul>
        `
    }
}

customElements.define("my-summary", MySummary)

Solution

  • I would remove this._loadSummary() from render(), otherwise you'll easily create an endless loop where you trigger an update, which triggers this._loadSummary, which triggers an update etc...

    this.food and this._rows must be reactive properties:

    static properties = {
      group: {
        type: String,
        state: true
      },
      _rows: {
        state: true
      }
    }

    Then run your function whenever group changes:

    updated(changedProperties) {
        changedProperties.forEach((oldValue, newValue) => {
            if (newValue === "group") {
              this._loadSummary()
            });
        }

    Then you can remove this._loadSummary from render and constructor, because it runs automatically whenever this.group is updated, and your component renders whenever this._rows changes.

    You can also bind this._loadSummary in your constructor to avoid having to write let that = this:

    constructor() {
      super()
      this.group = "all"
      this._rows = []
    
      this._loadSummary = this._loadSummary.bind(this);
    }