defineComponent is a factory function for creating reusable component classes. Give it a render function, and it returns a class whose instances produce that component’s virtual DOM. The same class can be instantiated repeatedly, while each instance can maintain its own state. section 9.2
Build the mechanism around these responsibilities:
- Accept render and optional state functions as configuration. section 9.3
- Create a Component class whose render method calls the supplied render function with the component as its context. section 9.3.2
- Initialize props and state in the constructor; let state(props) customize the initial state for each instance. section 9.3
- Provide mount, unmount, and updateState methods so instances can enter the DOM, leave it, and patch their rendered view. section 9.3.2
- Accept additional component methods for event handlers, binding them to the component context when needed. section 10.0
A counter with configurable initial state
const Counter = defineComponent({ state(props) { return { count: props.initialCount ?? 0 } }, render() { return /* virtual DOM based on this.state.count */ } }) const counter = new Counter({ initialCount: 10 }) counter.mount(document.body) The state function receives the instance’s props, so this counter starts at 10 instead of the default 0. Calling updateState({ count: 11 }) merges the new value and patches the view. section 9.3 section 9.3.2
Listing 9.6 The defineComponent() function so far (component.js)
import { destroyDOM } from './destroy-dom'
import { mountDOM } from './mount-dom'
import { patchDOM } from './patch-dom'
export function defineComponent({ render, state }) {
class Component {
#isMounted = false
#vdom = null
#hostEl = null
constructor(props = {}) {
this.props = props
this.state = state ? state(props) : {}
}
updateState(state) {
this.state = { ...this.state, ...state }
this.#patch()
}
render() {
return render.call(this)
}
mount(hostEl, index = null) {
if (this.#isMounted) {
throw new Error('Component is already mounted')
}
this.#vdom = this.render()
mountDOM(this.#vdom, hostEl, index)
this.#hostEl = hostEl
this.#isMounted = true
}
unmount() {
if (!this.#isMounted) {
throw new Error('Component is not mounted')
}
destroyDOM(this.#vdom)
this.#vdom = null
this.#hostEl = null
this.#isMounted = false
}
#patch() {
if (!this.#isMounted) {
throw new Error('Component is not mounted')
}
const vdom = this.render()
this.#vdom = patchDOM(this.#vdom, vdom, this.#hostEl)
}
}
return Component
}
Inspect the accumulated implementation, including props, optional initial state, rendering, mounting, unmounting, and state-driven DOM patching. section 9.3.2
