{"id":15384217,"url":"https://github.com/pmdartus/reactivity-in-depth","last_synced_at":"2026-01-07T00:37:09.649Z","repository":{"id":43008765,"uuid":"190704142","full_name":"pmdartus/reactivity-in-depth","owner":"pmdartus","description":null,"archived":false,"fork":false,"pushed_at":"2022-03-23T15:41:48.000Z","size":25,"stargazers_count":2,"open_issues_count":2,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-10-18T19:01:13.908Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pmdartus.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-06-07T07:24:33.000Z","updated_at":"2023-03-04T05:54:23.000Z","dependencies_parsed_at":"2022-09-09T13:52:04.199Z","dependency_job_id":null,"html_url":"https://github.com/pmdartus/reactivity-in-depth","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmdartus%2Freactivity-in-depth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmdartus%2Freactivity-in-depth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmdartus%2Freactivity-in-depth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmdartus%2Freactivity-in-depth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pmdartus","download_url":"https://codeload.github.com/pmdartus/reactivity-in-depth/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245937929,"owners_count":20696987,"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":[],"created_at":"2024-10-01T14:41:05.480Z","updated_at":"2026-01-07T00:37:09.617Z","avatar_url":"https://github.com/pmdartus.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Keeping the UI in sync with the state\n\nOn of the most complex challenge web building an application is to make sure the UI is in sync with the data. For simple widget, pure HTML and javascript can be sufficient. However as your application grows complexity reflecting the data updates back to the DOM (and vice-versa) becomes extremely hard and makes the overall application fragile.\n\nThat's where UI frameworks comes to the rescue. Framework makes developers' life easier by abstracting away the rendering details and automatically updates the DOM as the component state change. When it comes to reflect state change to the DOM, we can divide the work that need to be done by javascript framework in 2 different parts: detecting that a component state has changed and updating the UI accordingly.\n\nIn this article we will focus on the first aspect, and what the different ways to implement change detection.\n\n## Disclaimer\n\nEducational content, should not be used in production code.\nHighly simplified, focus on the high-level abstract and not on the internal details\n\n## Component model\n\nWill use raw web component in the rest for the rest of the article.\nNeed to define an abstraction on top of the web components.\n\n```js\nclass Component extends HTMLElement {\n    constructor() {\n        super();\n\n        this.attachShadow({\n            mode: 'open',\n        });\n\n        this._scheduleForRendering();\n    }\n\n    _scheduleForRendering() {\n        if (!this._willRender) {\n            this._willRender = true;\n\n            Promise.resolve().then(() =\u003e {\n                this._willRender = false;\n                this.render();\n            });\n        }\n    }\n\n    render() {}\n}\n```\n\nLet's create a base `Component` class to would hold the rendering and update logic. The class extends from `HTMLElement` because all the web components inherit from the `HTMLElement`.\n\n-   `constructor`: attach the shadow root and schedule the component for rendering.\n-   `_scheduleForRendering`: if the component is not already scheduled for rendering, the `_willRender` flag is set and invoke the `_invokeRender` method in the next micro task.\n-   `render`: is empty on purpose, it need to be overriden in the child class.\n\nLet's create a todo list example using the base `Component` class. First we need to define the the different template elements: one template for the todo list and a template for each todo list item.\n\n```html\n\u003ctemplate id=\"todo-list\"\u003e\n    \u003cstyle\u003e\n        span.completed {\n            text-decoration: line-through;\n        }\n    \u003c/style\u003e\n\n    \u003cinput id=\"todo-input\" /\u003e\n    \u003cul\u003e\u003c/ul\u003e\n\u003c/template\u003e\n\n\u003ctemplate id=\"todo-item\"\u003e\n    \u003cli\u003e\n        \u003cinput type=\"checkbox\" /\u003e\n        \u003cspan\u003e\u003c/span\u003e\n        \u003cbutton\u003e-\u003c/button\u003e\n    \u003c/li\u003e\n\u003c/template\u003e\n```\n\nThe todo list implementation is pretty naive at point, every class to `render` wipes out the entire shadow tree (via. `this.shadowRoot.innerHTML`) and rerender the entire todo list. Note, bad approach suffers from performance issues and poor user experience (focus loss).\n\n```js\nconst todoListTmpl = document.querySelector('#todo-list');\nconst todoItemTmpl = document.querySelector('#todo-item');\n\nclass XTodoList extends Component {\n    constructor() {\n        super();\n\n        this.nextId = 2;\n        this.todos = [\n            {\n                id: 1,\n                title: 'Learn reactivity',\n                completed: false,\n            },\n        ];\n    }\n\n    render() {\n        const items = this.todos.map(todo =\u003e {\n            const { id, title, completed } = todo;\n\n            const frag = todoItemTmpl.content.cloneNode(true);\n\n            const titleSpan = frag.querySelector('span');\n            titleSpan.textContent = title;\n            if (completed) {\n                titleSpan.classList.add('completed');\n            }\n\n            const completeCheckbox = frag.querySelector('input');\n            completeCheckbox.checked = completed;\n            completeCheckbox.addEventListener('click', () =\u003e\n                this.toggleCompleted(id),\n            );\n\n            const removeButton = frag.querySelector('button');\n            removeButton.addEventListener('click', () =\u003e\n                this.handleRemoveTodo(id),\n            );\n\n            return frag;\n        });\n\n        const frag = todoListTmpl.content.cloneNode(true);\n\n        const todoInput = frag.querySelector('input');\n        todoInput.addEventListener('change', () =\u003e\n            this.handleAddTodo(todoInput.value),\n        );\n\n        const list = frag.querySelector('ul');\n        list.append(...items);\n\n        this.shadowRoot.innerHTML = '';\n        this.shadowRoot.appendChild(frag);\n    }\n\n    handleAddTodo(title) {\n        this.todos.push({\n            id: this.nextId++,\n            title,\n            completed: false,\n        });\n\n        this._scheduleForRendering();\n    }\n\n    handleRemoveTodo(todoId) {\n        this.todos = this.todos.filter(todo =\u003e {\n            return todo.id !== todoId;\n        });\n\n        this._scheduleForRendering();\n    }\n\n    toggleCompleted(todoId) {\n        this.todos = this.todos.map(todo =\u003e {\n            return todo.id === todoId\n                ? { ...todo, completed: !todo.completed }\n                : todo;\n        });\n\n        this._scheduleForRendering();\n    }\n}\n\ncustomElements.define('x-todo-list', XTodoList);\n```\n\nNeed to invoke `this._scheduleForRendering` in order to reflect data updates back to the DOM: error prone.\n\n## Immutable state\n\nSome framework favors immutable state (like React). The `state` property in React is a special property used to store the local state of the component. From the [React official document](https://reactjs.org/docs/react-component.html#state):\n\n\u003e Never mutate `this.state` directly, as calling `setState()` afterwards may replace the mutation you made. Treat `this.state` as if it were immutable.\n\nBy invoking `setState`, React becomes aware of the state change and will schedule a new render invocation in the future.\n\nLet see how to implement `setState` on our base `Component` class:\n\n```js\nclass Component extends HTMLElement {\n    constructor() {\n        super();\n\n        this.attachShadow({\n            mode: 'open',\n        });\n\n        this.state = {};\n\n        this._scheduleForRendering();\n    }\n\n    setState(newState) {\n        this.state = newState;\n        this._scheduleForRendering();\n    }\n\n    _scheduleForRendering() {\n        if (!this._willRender) {\n            this._willRender = true;\n\n            Promise.resolve().then(() =\u003e {\n                this._willRender = false;\n                this.render();\n            });\n        }\n    }\n\n    render() {}\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpmdartus%2Freactivity-in-depth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpmdartus%2Freactivity-in-depth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpmdartus%2Freactivity-in-depth/lists"}