◆ WebComponents は使わない
◆ 機能的には state を変えたらコンポーネントだけ更新するだけ
  ◆ 書き方と作り方を React 風にしてる

lit-html は render の引数などを見ても React に近いものがあります
React で JSX を使ってるとパット見だとどっち?となりそうなくらいです

// React
ReactDOM.render(
<div><button id={id}>{name}</button></div>
document.body
)

// lit-html
render(
html`<div><button id=${id}>${name}</button></div>`
document.body
)

React 風コンポーネントを作る

lit-html 自体にはコンポーネント機能はなくて WebComponents の仕組みを使う前提で lit-element というライブラリがあります
WebComponents なしで lit-html だけで使いたい場合もありますし 書き方が近いなら React 風なコンポーネントが作れても良いと思うのでそれっぽいことをしてみます

とりあえず クラスと関数でコンポーネントを作れるようにします
React のクラスでは色々機能がついてますが 全部を作るのは大変なのでとりあえず state 機能だけにします
更新したら自動でそのコンポーネントだけ再 render します
ただ state の使い方の API は特に React の方法が好きでもないので自分好みに変えてます
constructor を書かなくても良いように 初期値は initial_state getter で定義し 更新は単純に state プロパティのプロパティを変更します

HTML 中にコンポーネントを指定する表現は ${} を使った埋め込みです
lit-html は JSX ではないですし WebComponents も使わないので ${} を使うしかないです
埋め込む値は独自の directive で createComponent 関数で作ります
createComponent 関数の 1 つ目の引数で関数またはクラスを 2 つ目の引数でコンポーネントにわたすプロパティを指定します
React.createElement みたいなものです

ライブラリ部分は長いのであとにして 使用例です

<!doctype html>

<div id="ex1"></div>
<hr>
<div id="ex2"></div>

<script type="module">
import { html, render, createComponent as c, LitComponent, useState } from "./lit-component.js"

// class component
class Counter1 extends LitComponent {
get initial_state() {
return {
num: this.props.initial,
}
}
up = () => {
this.state.num = this.state.num + 1
}
render() {
return html`
<div>
<button @click=${this.up}>Up</button>
<span>${this.state.num}</span>
</div>
`
}
}

render(
html`
<div>
${c(Counter1, { initial: 10 })}
${c(Counter1, { initial: 20 })}
</div>
`,
document.getElementById("ex1")
)

// function component
function Counter2(props) {
const [num, setNum] = useState(props.initial)

return html`
<div>
<button @click=${() => setNum(num + 1)}>Up</button>
<span>${num}</span>
</div>
`
}

render(
html`
<div>
${c(Counter2, { initial: 10 })}
${c(Counter2, { initial: 20 })}
</div>
`,
document.getElementById("ex2")
)
</script>

上半分はクラスコンポーネントで ex1 要素に render しています
下半分は関数コンポーネントで ex2 要素に render しています

React だと React.createElement や JSX に children の設定ができますが 今回作ったものでは特別に用意していません
createComponent の 2 つ目の引数のオブジェクトで

c(Counter2, { children: html`<div>AAA</div>` })

のようにプロパティとして lit-html のテンプレートを渡してコンポーネントの html`` 中に ${} を使って埋め込めばいいだけです
プロパティ名は children でもなんでもいいです

lit-component.js

ライブラリ部分はこんな感じです

import { render, directive } from "https://unpkg.com/lit-html"
export { html, render } from "https://unpkg.com/lit-html"

const wmap = new WeakMap()

export const createComponent = directive((component, props) => part => {
if (component.is_lit_component) {
if (!wmap.has(part)) {
const instance = new component()
wmap.set(part, instance)
instance.props = props || {}
instance.createState()
instance.update()
part.setValue(instance.fragment)
} else {
const instance = wmap.get(part)
instance.props = props || {}
instance.update()
part.setValue(instance.fragment)
}
} else {
if (!wmap.has(part)) {
wmap.set(part, { states: [], fragment: document.createDocumentFragment() })
}
const { states, fragment } = wmap.get(part)
const update = () => {
state_manager.setContext(states, update)
render(component(props), fragment)
}
update()
part.setValue(fragment)
}
})

export class LitComponent {
static is_lit_component = true
fragment = document.createDocumentFragment()

update() {
render(this.render(), this.fragment)
}

createState() {
const state = this.initial_state
if (!state) return
let scheduled = 0
const scheduleUpdate = () => {
scheduled++
Promise.resolve().then(() => {
if (--scheduled === 0) this.update()
})
}
this.state = Object.create(
null,
Object.fromEntries(
Object.keys(state).map(k => {
return [
k,
{
get() {
return state[k]
},
set(value) {
if (state[k] === value) return
state[k] = value
scheduleUpdate()
},
},
]
})
)
)
}
}

const state_manager = {
index: 0,
states: [],
callback: null,
setContext(states, callback) {
this.index = 0
this.states = states
this.callback = callback
},
get(init) {
const states = this.states
const index = this.index++
const callback = this.callback
if (!(index in states)) states[index] = init
const set = value => {
states[index] = value
callback && callback()
}
return [states[index], set]
},
}
export const useState = init => {
return state_manager.get(init)
}