{"id":13578177,"url":"https://github.com/atom/etch","last_synced_at":"2025-10-02T13:31:28.046Z","repository":{"id":49532139,"uuid":"39105425","full_name":"atom/etch","owner":"atom","description":"Builds components using a simple and explicit API around virtual-dom","archived":true,"fork":false,"pushed_at":"2022-09-28T10:52:02.000Z","size":305,"stargazers_count":555,"open_issues_count":8,"forks_count":57,"subscribers_count":29,"default_branch":"master","last_synced_at":"2025-01-04T04:24:15.571Z","etag":null,"topics":["components","javascript","virtual-dom"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/atom.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-07-14T23:22:29.000Z","updated_at":"2024-09-19T04:58:58.000Z","dependencies_parsed_at":"2022-09-05T13:02:06.568Z","dependency_job_id":null,"html_url":"https://github.com/atom/etch","commit_stats":null,"previous_names":[],"tags_count":38,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atom%2Fetch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atom%2Fetch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atom%2Fetch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atom%2Fetch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/atom","download_url":"https://codeload.github.com/atom/etch/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":235000841,"owners_count":18920257,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["components","javascript","virtual-dom"],"created_at":"2024-08-01T15:01:28.162Z","updated_at":"2025-10-02T13:31:27.716Z","avatar_url":"https://github.com/atom.png","language":"JavaScript","readme":"##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/)\n # ![Logo](https://cloud.githubusercontent.com/assets/378023/18806594/927cb104-826c-11e6-8e4b-7b54be52108e.png)\n\n[![CI](https://github.com/atom/etch/actions/workflows/ci.yml/badge.svg)](https://github.com/atom/etch/actions/workflows/ci.yml)\n\nEtch 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.\n\n### Overview\n\nEtch 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:\n\n```js\n/** @jsx etch.dom */\n\nconst etch = require('etch')\n\nclass MyComponent {\n  // Required: Define an ordinary constructor to initialize your component.\n  constructor (props, children) {\n    // perform custom initialization here...\n    // then call `etch.initialize`:\n    etch.initialize(this)\n  }\n\n  // Required: The `render` method returns a virtual DOM tree representing the\n  // current state of the component. Etch will call `render` to build and update\n  // the component's associated DOM element. Babel is instructed to call the\n  // `etch.dom` helper in compiled JSX expressions by the `@jsx` pragma above.\n  render () {\n    return \u003cdiv\u003e\u003c/div\u003e\n  }\n\n  // Required: Update the component with new properties and children.\n  update (props, children) {\n    // perform custom update logic here...\n    // then call `etch.update`, which is async and returns a promise\n    return etch.update(this)\n  }\n\n  // Optional: Destroy the component. Async/await syntax is pretty but optional.\n  async destroy () {\n    // call etch.destroy to remove the element and destroy child components\n    await etch.destroy(this)\n    // then perform custom teardown logic here...\n  }\n}\n```\n\nThe component defined above could be used as follows:\n\n```js\n// build a component instance in a standard way...\nlet component = new MyComponent({foo: 1, bar: 2})\n\n// use the component's associated DOM element however you wish...\ndocument.body.appendChild(component.element)\n\n// update the component as needed...\nawait component.update({bar: 2})\n\n// destroy the component when done...\nawait component.destroy()\n```\n\nNote 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.\n\n### Etch Lifecycle Functions\n\nUse 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.\n\n#### `etch.initialize(component)`\n\nThis 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.\n\nThis function is typically called at the end of your component's constructor:\n\n```js\n/** @jsx etch.dom */\n\nconst etch = require('etch')\n\nclass MyComponent {\n  constructor (properties) {\n    this.properties = properties\n    etch.initialize(this)\n  }\n\n  render () {\n    return \u003cdiv\u003e{this.properties.greeting} World!\u003c/div\u003e\n  }\n}\n\nlet component = new MyComponent({greeting: 'Hello'})\nconsole.log(component.element.outerHTML) // ==\u003e \u003cdiv\u003eHello World!\u003c/div\u003e\n```\n\n#### `etch.update(component[, replaceNode])`\n\nThis 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`.\n\n`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.\n\n`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:\n\n```js\n/** @jsx etch.dom */\n\nconst etch = require('etch')\n\nclass MyComponent {\n  constructor (properties) {\n    this.properties = properties\n    etch.initialize(this)\n  }\n\n  render () {\n    return \u003cdiv\u003e{this.properties.greeting} World!\u003c/div\u003e\n  }\n\n  update (newProperties) {\n    if (this.properties.greeting !== newProperties.greeting) {\n      this.properties.greeting = newProperties.greeting\n      return etch.update(this)\n    } else {\n      return Promise.resolve()\n    }\n  }\n}\n\n// in an async function...\n\nlet component = new MyComponent({greeting: 'Hello'})\nconsole.log(component.element.outerHTML) // ==\u003e \u003cdiv\u003eHello World!\u003c/div\u003e\nawait component.update({greeting: 'Salutations'})\nconsole.log(component.element.outerHTML) // ==\u003e \u003cdiv\u003eSalutations World!\u003c/div\u003e\n```\n\nThere 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.\n\n##### Update Hooks\n\nIf 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.\n\n* `writeAfterUpdate` If you need to *write* to any part of the document as a result of updating your component, you should perform these writes in an optional `writeAfterUpdate` method 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.\n\n* `readAfterUpdate` If you need to *read* any part of the document as a result of updating your component, you should perform these reads in an optional `readAfterUpdate` method defined on your component. You should avoid writing to the DOM in these methods, because writes could interleave with reads performed in `readAfterUpdate` hooks 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 via `etch.update`.\n\nThese 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.\n\n#### `etch.destroy(component[, removeNode])`\n\nWhen 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.\n\n`etch.destroy` is typically called in an async `destroy` method on the component:\n\n```js\nclass MyComponent {\n  // other methods omitted for brevity...\n\n  async destroy () {\n    await etch.destroy(this)\n\n    // perform component teardown... here we just log for example purposes\n    let greeting = this.properties.greeting\n    console.log(`Destroyed component with greeting ${greeting}`)\n  }\n}\n\n// in an async function...\n\nlet component = new MyComponent({greeting: 'Hello'})\ndocument.body.appendChild(component.element)\nassert(component.element.parentElement)\nawait component.destroy()\nassert(!component.element.parentElement)\n```\n\n### Component Composition\n\n#### Nesting Etch Components Within Other Etch Components\n\nComponents can be nested within other components by referencing a child component's constructor in the parent component's `render` method, as follows:\n\n```js\n/** @jsx etch.dom */\n\nconst etch = require('etch')\n\nclass ChildComponent {\n  constructor () {\n    etch.initialize(this)\n  }\n\n  render () {\n    return \u003ch2\u003eI am a child\u003c/h2\u003e\n  }\n}\n\nclass ParentComponent {\n  constructor () {\n    etch.initialize(this)\n  }\n\n  render () {\n    return (\n      \u003cdiv\u003e\n        \u003ch1\u003eI am a parent\u003c/div\u003e\n        \u003cChildComponent /\u003e\n      \u003c/div\u003e\n    )\n  }\n}\n```\n\nA constructor function can always take the place of a tag name in any Etch JSX expression. If the JSX expression has properties or children, these will be passed to the constructor function as the first and second argument, respectively.\n\n```js\n/** @jsx etch.dom */\n\nconst etch = require('etch')\n\nclass ChildComponent {\n  constructor (properties, children) {\n    this.properties = properties\n    this.children = children\n    etch.initialize(this)\n  }\n\n  render () {\n    return (\n      \u003cdiv\u003e\n        \u003ch2\u003eI am a {this.properties.adjective} child\u003c/h2\u003e\n        \u003ch2\u003eAnd these are *my* children:\u003c/h2\u003e\n        {this.children}\n      \u003c/div\u003e\n    )\n  }\n}\n\nclass ParentComponent {\n  constructor () {\n    etch.initialize(this)\n  }\n\n  render () {\n    return (\n      \u003cdiv\u003e\n        \u003ch1\u003eI am a parent\u003c/div\u003e\n        \u003cChildComponent adjective='good'\u003e\n          \u003cdiv\u003eGrandchild 1\u003c/div\u003e\n          \u003cdiv\u003eGrandchild 2\u003c/div\u003e\n        \u003c/ChildComponent\u003e\n      \u003c/div\u003e\n    )\n  }\n}\n```\n\nIf the properties or children change during an update of the parent component, Etch calls `update` on the child component with the new values. Finally, if an update causes the child component to no longer appear in the DOM or the parent component itself is destroyed, Etch will call `destroy` on the child component if it is implemented.\n\n#### Nesting Non-Etch Components Within Etch Components\n\nNothing about the component composition rules requires that the child component be implemented with Etch. So long as your constructor builds an object with an `.element` property and an `update` method, it can be nested within an Etch virtual DOM tree. Your component can also implement `destroy` if you want to perform teardown logic when it is removed from the parent component.\n\nThis feature makes it easy to mix components written in different versions of Etch or wrap components written in other technologies for integration into an Etch component. You can even just use raw DOM APIs for simple or performance-critical components and use them straightforwardly within Etch.\n\n### Keys\n\nTo keep DOM update times linear in the size of the virtual tree, Etch applies a very simple strategy when updating lists of elements. By default, if a child at a given location has the same tag name in both the previous and current virtual DOM tree, Etch proceeds to apply updates for the entire subtree.\n\nIf your virtual DOM contains a list into which you are inserting and removing elements frequently, you can associate each element in the list with a unique `key` property to identify it. This improves performance by allowing Etch to determine whether a given element tree should be *inserted* as a new DOM node, or whether it corresponds to a node that already exists that needs to be *updated*.\n\n### References\n\nEtch interprets any `ref` property on a virtual DOM element as an instruction to wire a reference to the underlying DOM element or child component. These references are collected in a `refs` object that Etch assigns on your component.\n\n```js\nclass ParentComponent {\n  constructor () {\n    etch.initialize(this)\n  }\n\n  render () {\n    return (\n      \u003cdiv\u003e\n        \u003cspan ref='greetingSpan'\u003eHello\u003c/span\u003e\n        \u003cChildComponent ref='childComponent' /\u003e\n      \u003c/div\u003e\n    )\n  }\n}\n\nlet component = new ParentComponent()\ncomponent.refs.greetingSpan // This is a span DOM node\ncomponent.refs.childComponent // This is a ChildComponent instance\n```\nNote that `ref` properties on normal HTML elements create references to raw DOM nodes, while `ref` properties on child components create references to the constructed component object, which makes its DOM node available via its `element` property.\n\n### Handling Events\n\nEtch supports listening to arbitrary events on DOM nodes via the special `on` property, which can be used to assign a hash of `eventName: listenerFunction` pairs:\n\n```js\nclass ComponentWithEvents {\n  constructor () {\n    etch.initialize(this)\n  }\n\n  render () {\n    return \u003cdiv on={{click: this.didClick, focus: this.didFocus}} /\u003e\n  }\n\n  didClick (event) {\n    console.log(event) // ==\u003e MouseEvent {...}\n    console.log(this) // ==\u003e ComponentWithEvents {...}\n  }\n\n  didFocus (event) {\n    console.log(event) // ==\u003e FocusEvent {...}\n    console.log(this) // ==\u003e ComponentWithEvents {...}\n  }\n}\n```\n\nAs you can see, the listener function's `this` value is automatically bound to the parent component. You should rely on this auto-binding facility rather than using arrow functions or `Function.bind` to avoid complexity and extraneous closure allocations.\n\n### Assigning DOM Attributes\n\nWith the exception of SVG elements, Etch assigns *properties* on DOM nodes rather than HTML attributes. If you want to bypass this behavior and assign attributes instead, use the special `attributes` property with a nested object. For example, `a` and `b` below will yield the equivalent DOM node.\n\n```js\nconst a = \u003cdiv className='foo' /\u003e\nconst b = \u003cdiv attributes={{class: 'foo'}} /\u003e\n```\n\nThis can be useful for custom attributes that don't map to DOM node properties.\n\n### Organizing Component State\n\nTo keep the API surface area minimal, Etch is deliberately focused only on updating the DOM, leaving management of component state to component authors.\n\n#### Controlled Components\n\nIf your component's HTML is based solely on properties passed in from the outside, you just need to implement a simple `update` method.\n\n```js\nclass ControlledComponent {\n  constructor (props) {\n    this.props = props\n    etch.initialize(this)\n  }\n\n  render () {\n    // read from this.props here\n  }\n\n  update (props) {\n    // you could avoid redundant updates by comparing this.props with props...\n    this.props = props\n    return etch.update(this)\n  }\n}\n```\n\nCompared to React, control is inverted. Instead of implementing `shouldComponentUpdate` to control whether or not the framework updates your element, you always explicitly call `etch.update` when an update is needed.\n\n#### Stateful Components\n\nIf your `render` method's output is based on state managed within the component itself, call `etch.update` whenever this state is updated. You could store all state in a sub-object called `state` like React does, or you could just use instance variables.\n\n```js\nclass StatefulComponent {\n  constructor () {\n    this.counter = 0\n    etch.initialize(this)\n  }\n\n  render () {\n    return (\n      \u003cdiv\u003e\n        \u003cspan\u003e{this.counter}\u003c/span\u003e\n        \u003cbutton onclick={() =\u003e this.incrementCounter()}\u003e\n          Increment Counter\n        \u003c/button\u003e\n      \u003c/div\u003e\n    )\n  }\n\n  incrementCounter () {\n    this.counter++\n    // since we updated state we use in render, call etch.update\n    return etch.update(this)\n  }\n}\n```\n\n#### What About A Component Superclass?\n\nTo keep this library small and explicit, we're favoring composition over inheritance. Etch gives you a small set of tools for updating the DOM, and with these you can accomplish your objectives with some simple patterns. You *could* write a simple component superclass in your application to remove a bit of boilerplate, or even publish one on npm. For now, however, we're going to avoid taking on the complexity of such a superclass into this library. We may change our mind in the future.\n\n### Customizing The Scheduler\n\nEtch exports a `setScheduler` method that allows you to override the scheduler it uses to coordinate DOM writes. When using Etch inside a larger application, it may be important to coordinate Etch's DOM interactions with other libraries to avoid synchronous reflows.\n\nFor example, when using Etch in Atom, you should set the scheduler as follows:\n\n```js\netch.setScheduler(atom.views)\n```\n\nRead comments in the [scheduler assignment][scheduler-assignment] and [default scheduler][default-scheduler] source code for more information on implementing your own scheduler.\n\n### Performance\n\nThe [github.com/krausest/js-framework-benchmark](https://github.com/krausest/js-framework-benchmark) runs various benchmarks using different frameworks. It should give you an idea how etch performs compared to other frameworks.\n\nCheckout the benchmarks [here](https://rawgit.com/krausest/js-framework-benchmark/master/webdriver-ts-results/table.html).\n\n### Feature Requests\n\nEtch aims to stay small and focused. If you have a feature idea, consider implementing it as a library that either wraps Etch or, even better, that can be used in concert with it. If it's impossible to implement your feature outside of Etch, we can discuss adding a hook that makes your feature possible.\n\n[babel]: https://babeljs.io/\n[scheduler-assignment]: https://github.com/nathansobo/etch/blob/master/lib/scheduler-assignment.js\n[default-scheduler]: https://github.com/nathansobo/etch/blob/master/lib/default-scheduler.js\n[dom-listener]: https://github.com/atom/dom-listener\n","funding_links":[],"categories":["JavaScript","Frameworks"],"sub_categories":["Rest of the Pack"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fatom%2Fetch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fatom%2Fetch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fatom%2Fetch/lists"}