◆ 親コンポーネントから渡されるプロパティも properties で定義必要
  ◆ 更新されたときに render が必要なら properties に書いとく
◆ 配列に push したり オブジェクトのプロパティ更新では render されない
  ◆ requestUpdate で子の setter は呼び出せるが子の setter で変更なし扱いされて render されない
  ◆ ⇨ 子の requestUpdate を呼び出せば更新できる
  ◆ ⇨ イミュータブルにしてオブジェクト自体を新しく作ってプロパティ更新
  ◆ ⇨ setter を自作して requestUpdate を呼び出すようにしておく

LitElement の親コンポーネントと子コンポーネントを用意します
親には num と items プロパティがあって両方とも 子コンポーネントに渡します

<!DOCTYPE html>
<meta charset="utf-8" />

<script type="module">
import { LitElement, html, css } from "https://unpkg.com/lit-element?module"

customElements.define("elem-parent", class extends LitElement {
static properties = {
num: { type: Number },
items: { type: Array },
}

constructor() {
super()
this.num = 100
this.items = [1, 2, 3]
}

render() {
return html`
<elem-child .num=${this.num} .items=${this.items}></elem-child>
`
}
})

customElements.define("elem-child", class extends LitElement {
render() {
return html`
<div>${this.num}</div>
<div>${this.items.join("_")}</div>
`
}
})

LitElement.render(html`<elem-parent></elem-parent>`, document.body, { scopeName: "ROOT" })
</script>

この状態で

document.querySelector("elem-parent").num = 101
document.querySelector("elem-parent").items = [10, 20]

を実行すると elem-child のプロパティも更新されます
しかし render 関数は実行されないので画面は変わりません

elem-child でも変更されると再 render してほしい部分は properties に設定しておきます

customElements.define("elem-child", class extends LitElement {
static properties = {
num: { type: Number },
items: { type: Array },
}

render() {
return html`
<div>${this.num}</div>
<div>${this.items.join("_")}</div>
`
}
})

これで更新できるようになりました

しかし

document.querySelector("elem-parent").items.push(200)

のような操作では更新されません
これはオブジェクト内部の操作であって items プロパティ自身の参照は変更されていません
なので変更があったことを検出できず render は実行されません

document.querySelector("elem-parent").requestUpdate()

を実行すれば elem-parent の render が実行され items へ代入が行われます
しかし これでも画面は更新されません
properties を書くことで自動で生成される setter の内部では代入時に変更があったかを判断して 変更がないなら render を行いません
強制的に render させるために

document.querySelector("elem-parent").shadowRoot.querySelector("elem-child").requestUpdate()

が必要です
elem-child の requestUpdate メソッドを呼び出します
参照が遠いですし items を複数のコンポーネントに渡していたら全部の子コンポーネントで実行しないといけません
多くなると嫌になる作業です

単純な方法は 最初の方法のように items を更新してしまうこと

const p = document.querySelector("elem-parent")
p.items = [...p.items, 200]

こうすれば参照が異なり items 自体が別物になるので setter の処理で render が行われます
毎回作るのは無駄ですが 要素が何万もあって再作成を何度も繰り返すわけでもないならパフォーマンス的にはさほど影響しません

let a = Array(1000).fill(0)
console.time()
for(let i=0;i<1000;i++){
a = [...a, i]
}
console.timeEnd()
// default: 6.40673828125ms
let a = Array(10000).fill(0)
console.time()
for(let i=0;i<1000;i++){
a = [...a, i]
}
console.timeEnd()
// default: 52.60595703125ms

ちょっとしたことですが いまのところは [...arr] で結合より concat のほうが速いです
内部的に values() のイテレータで全件取得してるからでしょう
イテレータの next メソッドを呼び出してるなら 1 つ取得ごとに 1 関数実行ですし
Symbol.iterator の値が配列デフォルトのものなら concat とおなじになるはずですし concat に置き換えて実行みたいな最適化がされるといいのですけどねー

let a = Array(10000).fill(0)
console.time()
for(let i=0;i<1000;i++){
a = a.concat([i])
}
console.timeEnd()
// default: 21.431884765625ms

同じ参照であることに依存しない作りにしてれば 上の方法で解決できますが そうできない場合もあります
親側の requestUpdate で setter 自体は呼び出されるので 自動生成に頼らず自分で setter プロパティを作って更新する方法もあります

customElements.define("elem-child", class extends LitElement {
static properties = {
num: { type: Number },
}

_items = null
get items() {
return this._items
}
set items(value) {
this._items = value
this._requestUpdate()
}

render() {
return html`
<div>${this.num}</div>
<div>${this.items.join("_")}</div>
`
}
})

子コンポーネントをこうしておけば親コンポーネントでは自分の requestUpdate を呼び出すだけで済みます

document.querySelector("elem-parent").items.push(200)
document.querySelector("elem-parent").requestUpdate()