Etch
Builds components using a simple and explicit API around virtual-dom
Install / Use
/learn @atom/EtchREADME
Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our official announcement
Etch is a library for writing HTML-based user interface components that provides the convenience of a virtual DOM, while at the same time striving to be minimal, interoperable, and explicit. Etch can be used anywhere, but it was specifically designed with Atom packages and Electron applications in mind.
Overview
Etch components are ordinary JavaScript objects that conform to a minimal interface. Instead of inheriting from a superclass or building your component with a factory method, you access Etch's functionality by passing your component to Etch's library functions at specific points of your component's lifecycle. A typical component is structured as follows:
/** @jsx etch.dom */
const etch = require('etch')
class MyComponent {
// Required: Define an ordinary constructor to initialize your component.
constructor (props, children) {
// perform custom initialization here...
// then call `etch.initialize`:
etch.initialize(this)
}
// Required: The `render` method returns a virtual DOM tree representing the
// current state of the component. Etch will call `render` to build and update
// the component's associated DOM element. Babel is instructed to call the
// `etch.dom` helper in compiled JSX expressions by the `@jsx` pragma above.
render () {
return <div></div>
}
// Required: Update the component with new properties and children.
update (props, children) {
// perform custom update logic here...
// then call `etch.update`, which is async and returns a promise
return etch.update(this)
}
// Optional: Destroy the component. Async/await syntax is pretty but optional.
async destroy () {
// call etch.destroy to remove the element and destroy child components
await etch.destroy(this)
// then perform custom teardown logic here...
}
}
The component defined above could be used as follows:
// build a component instance in a standard way...
let component = new MyComponent({foo: 1, bar: 2})
// use the component's associated DOM element however you wish...
document.body.appendChild(component.element)
// update the component as needed...
await component.update({bar: 2})
// destroy the component when done...
await component.destroy()
Note that using an Etch component does not require a reference to the Etch library. Etch is an implementation detail, and from the outside the component is just an ordinary object with a simple interface and an .element property. You can also take a more declarative approach by embedding Etch components directly within other Etch components, which we'll cover later in this document.
Etch Lifecycle Functions
Use Etch's three lifecycle functions to associate a component with a DOM element, update that component's DOM element when the component's state changes, and tear down the component when it is no longer needed.
etch.initialize(component)
This function associates a component object with a DOM element. Its only requirement is that the object you pass to it has a render method that returns a virtual DOM tree constructed with the etch.dom helper ([Babel][babel] can be configured to compile JSX expressions to etch.dom calls). This function calls render and uses the result to build a DOM element, which it assigns to the .element property on your component object. etch.initialize also assigns any references (discussed later) to a .refs object on your component.
This function is typically called at the end of your component's constructor:
/** @jsx etch.dom */
const etch = require('etch')
class MyComponent {
constructor (properties) {
this.properties = properties
etch.initialize(this)
}
render () {
return <div>{this.properties.greeting} World!</div>
}
}
let component = new MyComponent({greeting: 'Hello'})
console.log(component.element.outerHTML) // ==> <div>Hello World!</div>
etch.update(component[, replaceNode])
This function takes a component that is already associated with an .element property and updates the component's DOM element based on the current return value of the component's render method. If the return value of render specifies that the DOM element type has changed since the last render, Etch will switch out the previous DOM node for the new one unless replaceNode is false.
etch.update is asynchronous, batching multiple DOM updates together in a single animation frame for efficiency. Even if it is called repeatedly with the same component in a given event-loop tick, it will only perform a single DOM update per component on the next animation frame. That means it is safe to call etch.update whenever your component's state changes, even if you're doing so redundantly. This function returns a promise that resolves when the DOM update has completed.
etch.update should be called whenever your component's state changes in a way that affects the results of render. For a basic component, you can implement an update method that updates your component's state and then requests a DOM update via etch.update. Expanding on the example from the previous section:
/** @jsx etch.dom */
const etch = require('etch')
class MyComponent {
constructor (properties) {
this.properties = properties
etch.initialize(this)
}
render () {
return <div>{this.properties.greeting} World!</div>
}
update (newProperties) {
if (this.properties.greeting !== newProperties.greeting) {
this.properties.greeting = newProperties.greeting
return etch.update(this)
} else {
return Promise.resolve()
}
}
}
// in an async function...
let component = new MyComponent({greeting: 'Hello'})
console.log(component.element.outerHTML) // ==> <div>Hello World!</div>
await component.update({greeting: 'Salutations'})
console.log(component.element.outerHTML) // ==> <div>Salutations World!</div>
There is also a synchronous variant, etch.updateSync, which performs the DOM update immediately. It doesn't skip redundant updates or batch together with other component updates, so you shouldn't really use it unless you have a clear reason.
Update Hooks
If you need to perform imperative DOM interactions in addition to the declarative updates provided by etch, you can integrate your imperative code via update hooks on the component. To ensure good performance, it's important that you segregate DOM reads and writes in the appropriate hook.
-
writeAfterUpdateIf you need to write to any part of the document as a result of updating your component, you should perform these writes in an optionalwriteAfterUpdatemethod defined on your component. Be warned: If you read from the DOM inside this method, it could potentially lead to layout thrashing by interleaving your reads with DOM writes associated with other components. -
readAfterUpdateIf you need to read any part of the document as a result of updating your component, you should perform these reads in an optionalreadAfterUpdatemethod defined on your component. You should avoid writing to the DOM in these methods, because writes could interleave with reads performed inreadAfterUpdatehooks defined on other components. If you need to update the DOM as a result of your reads, store state on your component and request an additional update viaetch.update.
These hooks exist to support DOM reads and writes in response to Etch updating your component's element. If you want your hook to run code based on changes to the component's logical state, you can make those calls directly or via other mechanisms. For example, if you simply want to call an external API when a property on your component changes, you should move that logic into the update method.
etch.destroy(component[, removeNode])
When you no longer need a component, pass it to etch.destroy. This function will call destroy on any child components (child components are covered later in this document), and will additionally remove the component's DOM element from the document unless removeNode is false. etch.destroy is also asynchronous so that it can combine the removal of DOM elements with other DOM updates, and it returns a promise that resolves when the component destruction process has completed.
etch.destroy is typically called in an async destroy method on the component:
class MyComponent {
// other methods omitted for brevity...
async destroy () {
await etch.destroy(this)
// perform component teardown... here we just log for example purposes
let greeting = this.properties.greeting
console.log(`Destroyed component with greeting ${greeting}`)
}
}
// in an async function...
let component = new MyComponent({greeting: 'Hello'})
document.body.appendChild(component.element)
assert(component.element.parentElement)
await component.destroy()
assert(!component.element.parentElement)
Component Composition
Nesting Etch Components Within Other Etch Components
Components can be nested within other components by referencing a child component's constructor in the parent component's render method, as follows:
/** @jsx etch.dom */
const etch = require('etch')
class ChildComponent {
constructor () {
etch.initialize(this)
}
render () {
return <h2>I am a child</h2>
}
}
class ParentComponent {
constructor () {
etch.initialize(this)
}
render () {
return (
<div>
<h1>I am a parent</div>
<ChildComponent />

