{"id":18563152,"url":"https://github.com/compulim/react-augmentation","last_synced_at":"2026-07-04T03:04:44.472Z","repository":{"id":82478929,"uuid":"105525662","full_name":"compulim/react-augmentation","owner":"compulim","description":"DOM augmentation operators to simplify UI code and promote data-driven DOM hierarchy","archived":false,"fork":false,"pushed_at":"2017-10-09T05:49:16.000Z","size":157,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-25T01:53:52.430Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/compulim.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-10-02T11:11:19.000Z","updated_at":"2017-10-06T05:19:06.000Z","dependencies_parsed_at":"2023-06-07T21:45:24.599Z","dependency_job_id":null,"html_url":"https://github.com/compulim/react-augmentation","commit_stats":{"total_commits":33,"total_committers":1,"mean_commits":33.0,"dds":0.0,"last_synced_commit":"c40a73a4f854dc338a8c67acc88311c17c99459a"},"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Freact-augmentation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Freact-augmentation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Freact-augmentation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Freact-augmentation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/compulim","download_url":"https://codeload.github.com/compulim/react-augmentation/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254394726,"owners_count":22063984,"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-11-06T22:11:55.968Z","updated_at":"2025-10-30T10:09:13.339Z","avatar_url":"https://github.com/compulim.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# react-augmentation\n\n[![Build Status](https://travis-ci.org/compulim/react-augmentation.svg?branch=master)](https://travis-ci.org/compulim/react-augmentation)\n\nDOM augmentation operators to simplify UI code and promote [data-driven DOM hierarchy](#what-is-data-driven-dom-hierarchy).\n\n# Background\n\n*Disclaimer: everything in this library is pure React. Absolutely no DOM manipulation.*\n\nAt first, augmentation operators may be mind-bending, unnecessary, and often feels like [Inception](http://www.imdb.com/title/tt1375666/), or even worse, anti-pattern. They enable scenarios that could otherwise very cumbersome or difficult to implement.\n\n## What is data-driven DOM hierarchy?\n\nUse JSX as how it is designed to use. Support rich, live, and eventful content.\n\n### Passing array of items\n\nAssume you want to pass a list of item to render.\n\n#### Anti-pattern\n\n```jsx\n\u003cList items={ ['Buy eggs', 'Buy milk'] } /\u003e\n```\n\nThis is bad, because array passed into `\u003cList\u003e` can differ from time to time even they have same content. React use reference equal to look for changes. You can use memoize function to patch the issue.\n\n#### The right way\n\n```jsx\n\u003cList\u003e\n  \u003cList.Item\u003eBuy eggs\u003c/List.Item\u003e\n  \u003cList.Item\u003eBuy milk\u003c/List.Item\u003e\n\u003c/List\u003e\n```\n\nUse `props.children` to pass items. As a bonus, you can also pass rich and live content.\n\nYou may prefer the anti-pattern version of `\u003cList\u003e` because you want to have temporal or permanent control on the lifetime of the items, e.g. for fade out animation. If you allow the user to remove item as needed, it will either break animation, or make the component very complicated to use.\n\n### Deterministic and traceability\n\nConsider you want to show a modal on topmost layer (last child of `\u003cbody\u003e`). You may end up either using a global store or build a function, that would be `this.props.dispatch(ModalActions.show())` or `MessageBox.show()` respectively.\n\nThen later on, when you want to put rich and live content in the dialog box, you will need to update the global store or call `MessageBox.show()` continuously. What's worse, if you need to put an `\u003cinput\u003e` in the modal and receive its event rightaway, it will become difficult and complicated.\n\nUpdating the modal manually and continuously is a plausible workaround, but you will be trading deterministic and traceability. Debugging would become difficult because lots of function calls. And saving the current browser state become a complicated business because the modal is not declarative and non-deterministic.\n\nThe better way is to use `render()` to create the modal, probably close to where it is needed. But keeping the modal in the topmost layer means you need to manipulate DOM manually, which is a big no-no for React.\n\n# Operators\n\nWe build a list of augmentation operators to make data-driven DOM hierarchy a possibility:\n\n* [Pipe](#pipe)\n* [Persistence of vision](#persistence-of-vision)\n\n## Pipe\n\n### How it works?\n\nWhen you put elements in `\u003cInlet\u003e`, they will be rendered to `\u003cOutlet\u003e` with same `name`.\n\n#### Your code\n\n```jsx\n\u003cbody\u003e\n  \u003cPipeProvider className=\"pipe-provider\"\u003e\n    \u003cdiv className=\"inlet\"\u003e\n      \u003cInlet name=\"greeting\"\u003e\n        \u003cp\u003eHello, World!\u003c/p\u003e\n      \u003c/Inlet\u003e\n    \u003c/div\u003e\n    \u003cdiv className=\"outlet\"\u003e\n      \u003cOutlet name=\"greeting\" /\u003e\n    \u003c/div\u003e\n  \u003c/PipeProvider\u003e\n\u003c/body\u003e\n```\n\n#### Would become\n\n```html\n\u003cbody\u003e\n  \u003cdiv class=\"pipe-provider\"\u003e\n    \u003cdiv class=\"inlet\"\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"outlet\"\u003e\n      \u003cdiv\u003e\n        \u003cp\u003eHello, World!\u003c/p\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/body\u003e\n```\n\n### Why you need it?\n\nWhen the user click \"Submit\", you may want to create a new `\u003cModal\u003e` and append to `\u003cbody\u003e`. This confirm modal will be on top of everything on the page, like this DOM hierarchy:\n\n```jsx\n\u003cbody\u003e\n  \u003cform\u003e\n    \u003cbutton onClick={ this.handleSubmit }\u003eSubmit\u003c/button\u003e\n  \u003c/form\u003e\n  \u003cModal title=\"Confirm\"\u003e\n    \u003cp\u003eAre you sure to submit the form?\u003c/p\u003e\n    \u003cbutton\u003eYes\u003c/button\u003e\n    \u003cbutton\u003eNo\u003c/button\u003e\n  \u003c/Modal\u003e\n\u003c/body\u003e\n```\n\nBut it might not be easily done because at the time you create the modal, you might be deep in the DOM. To talk to outmost DOM hierarchy that is not a direct ancestor, that means you either need a global store like Redux, or writing a modal service using context.\n\nFurthermore, if the content of the dialog box has rich or live content, it will be complicated to implement using Redux.\n\n### How you use it?\n\nYou wrap `\u003cModal\u003e` in `\u003cInlet\u003e` and put side-by-side to where its lifetime is managed. Then you put `\u003cOutlet\u003e` as the last child of `\u003cbody\u003e`. When `\u003cInlet\u003e` get rendered, all of its content will be piped to `\u003cOutlet\u003e`.\n\n```jsx\n\u003cbody\u003e\n  \u003cPipeProvider\u003e\n    \u003cform\u003e\n      \u003cbutton onClick={ this.handleSubmit }\u003eSubmit\u003c/button\u003e\n      \u003cInlet name=\"modal\"\u003e\n        \u003cModal title=\"Confirm\"\u003e\n          \u003cp\u003eAre you sure to submit the form?\u003c/p\u003e\n          \u003cbutton\u003eYes\u003c/button\u003e\n          \u003cbutton\u003eNo\u003c/button\u003e\n        \u003c/Modal\u003e\n      \u003c/Inlet\u003e\n    \u003c/form\u003e\n    \u003cOutlet name=\"modal\" /\u003e\n  \u003c/PipeProvider\u003e\n\u003c/body\u003e\n```\n\nSimilar to Redux `\u003cProvider\u003e`, which use React context intensively. To support pipe operator, you need to wrap one of the common ancestors of `\u003cInlet\u003e` and `\u003cOutlet\u003e`, with `\u003cPipeProvider\u003e`. Usually, you wrap the topmost layer of your app.\n\n## Persistence of vision\n\n### How it works?\n\nReviving an unmounted element temporarily for animation. The revived element is a frozen shallow copy. It is designed to be non-deterministic to demote its usage for extended time.\n\nInitially, you write\n\n```jsx\n\u003cPersistenceOfVision\u003e\n  \u003cp\u003eHello, World!\u003c/p\u003e\n\u003c/PersistenceOfVision\u003e\n```\n\nReact will render it as\n\n```html\n\u003cdiv\u003e\n  \u003cp\u003eHello, World!\u003c/p\u003e\n\u003c/div\u003e\n```\n\nThen you mutate it into\n\n```jsx\n\u003cPersistenceOfVision /\u003e\n```\n\nReact will render it as the children is never unmounted, like\n\n```html\n\u003cdiv\u003e\n  \u003cp\u003eHello, World!\u003c/p\u003e\n\u003c/div\u003e\n```\n\nUnmounted children inside `\u003cPersistenceOfVision\u003e` will be persisted temporarily to enable animation.\n\n### Why you need it?\n\nFor example, you implement an UI library for a beautiful list component named `\u003cList\u003e`. You expect user of your `\u003cList\u003e` component should produce a DOM hierarchy like this:\n\n```jsx\n\u003cList\u003e\n  \u003cList.Item\u003eBuy milk\u003c/List.Item\u003e\n  \u003cList.Item\u003eBuy eggs\u003c/List.Item\u003e\n\u003c/List\u003e\n```\n\nWhen \"Buy eggs\" is removed, you want to make it fade out rather than removing it immediately. But since the lifetime of `\u003cList.Item\u003e` is controlled by the user of your UI library. When `\u003cList.Item\u003eBuy eggs\u003c/List.Item\u003e` is unmounted, it will be removed immediately. Thus, you cannot implement a fade out animation.\n\nYou can opt for an alternative solution by forcing item lifetime controlled by `\u003cList\u003e`, like this:\n\n```jsx\n\u003cList items={ ['Buy milk', 'Buy eggs'] } /\u003e\n```\n\nBut there are few disadvantages to this pattern:\n\n* It is easy to produce wasted render because of careless user create array during `render()` loop, impacting performance\n* Supporting rich and live content is not trivial\n\n### How you use it?\n\nInside `\u003cList\u003e`, you deploy `\u003cPersistenceOfVision\u003e`:\n\n```js\nclass List extends React.Component {\n  componentWillReceiveProps(nextProps) {\n    // Remember how many items were removed\n    // In real world, you should also remember where the item is removed\n    this.setState(() =\u003e ({\n      numItemsRemoved: Math.max(0, this.props.items.length - nextProps.items.length)\n    }))\n  }\n\n  render() {\n    // It is important to add empty items and rendering the list together with existing items\n    // This is because the internal handling of key in React, explained in caveats section\n    const allItems = this.items.concat(new Array(this.state.numItemsRemoved).fill());\n\n    return (\n      \u003cul\u003e\n        {\n          allItems.map(item =\u003e\n            \u003cPersistenceOfVision\n              className   ={ item ? '' : 'fade-out' }\n              wrappingType=\"li\"\n            \u003e\n              { item }\n            \u003c/PersistenceOfVision\u003e\n          )\n        }\n      \u003c/ul\u003e\n    );\n  }\n}\n```\n\nBefore the user remove \"Buy eggs\", React render this:\n\n```jsx\n\u003cList\u003e\n  \u003cPersistenceOfVision\u003eBuy milk\u003c/PersistenceOfVision\u003e\n  \u003cPersistenceOfVision\u003eBuy eggs\u003c/PersistenceOfVision\u003e\n\u003c/List\u003e\n```\n\nWhich become\n\n```html\n\u003cul\u003e\n  \u003cli\u003eBuy milk\u003c/li\u003e\n  \u003cli\u003eBuy eggs\u003c/li\u003e\n\u003c/ul\u003e\n```\n\nAfter we removed \"Buy eggs\" from the list, we still render two `\u003cPersistenceOfVision\u003e`, like this:\n\n```jsx\n\u003cList\u003e\n  \u003cPersistenceOfVision\u003eBuy milk\u003c/PersistenceOfVision\u003e\n  \u003cPersistenceOfVision className=\"fade-out\" /\u003e\n\u003c/List\u003e\n```\n\nWhich would become\n\n```html\n\u003cul\u003e\n  \u003cli\u003eBuy milk\u003c/li\u003e\n  \u003cli class=\"fade-out\"\u003eBuy eggs\u003c/li\u003e\n\u003c/ul\u003e\n```\n\nAlthough the last one does not have any content, `\u003cPersistenceOfVision\u003e` will temporarily revive it so you can apply the `fade-out` CSS animation.\n\n### Caveats\n\n#### Temporal effect\n\nPersistence of vision should only be used for short period of time. This is because the effect is temporal and its non-deterministic nature.\n\n#### Using \"key\" props\n\nIf you are using \"key\" props in an array of `\u003cPersistenceOfVision\u003e`, make sure you render existing and missing `\u003cPoV\u003e` in the same `Array.map()` loop. Consider the keys in between these two scenarios:\n\n```jsx\n\u003cul\u003e\n  { [\u003cdiv key=\"1\"\u003eABC\u003c/div\u003e, \u003cdiv key=\"2\"\u003eDEF\u003c/div\u003e] }\n\u003c/ul\u003e\n```\n\nAnd\n\n```jsx\n\u003cul\u003e\n  { [\u003cdiv key=\"1\"\u003eABC\u003c/div\u003e] }\n  { [\u003cdiv key=\"2\"\u003eDEF\u003c/div\u003e] }\n\u003c/ul\u003e\n```\n\nAlthough two outcomes are the same, the keys of two \"DEF\" are different. When React is reconciling the first scenario into the second, \"DEF\" will get destroyed and reconstructed. This is because the first \"DEF\" is rendered in the *first* array, and the second \"DEF\" is rendered in *another* array. Their key can never be the same regardless of its actual value.\n\n# What's next?\n\nWe have created a [`react-augmentation-sandbox`](https://github.com/compulim/react-augmentation-sandbox) repository to let everyone to hack the augmentation operators.\n\n# Contributions\n\nLike us? [Star](https://github.com/compulim/react-augmentation/stargazers) us.\n\nFound an issue? [File](https://github.com/compulim/react-augmentation/issues) a minimal repro to us.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcompulim%2Freact-augmentation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcompulim%2Freact-augmentation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcompulim%2Freact-augmentation/lists"}