Reactive DOM
Download the CJS, ESM, UMD versions or install via NPM:
npm install @ryanmorr/reflexReflex is a small, but versatile UI library that combines declarative DOM building with reactive stores that bind data to DOM nodes, automatically keeping the DOM in sync when the data is changed:
import{html,store}from'@ryanmorr/reflex';constcount=store(0);constelement=html`<div><p>Count: ${count}</p><buttononclick=${()=>count.update((val)=>val+1)}>Increment</button></div>`;document.body.appendChild(element);Create a reactive store that encapsulates a value and can notify subscribers when the value changes:
import{store}from'@ryanmorr/reflex';// Create a store with an initial valueconstcount=store(0);// Get the store valuecount.value();//=> 0// Set the store valuecount.set(1);// Set the store value with a callback functioncount.update((val)=>val+1);// Subscribe a callback function to be invoked when the value changes,// it returns a function to unsubscribe from future updatesconstunsubscribe=count.subscribe((nextVal,prevVal)=>{// Do something});Create a reactive store that is based on the value of one or more other stores:
import{derived,store}from'@ryanmorr/reflex';constfirstName=store('John');constlastName=store('Doe');constfullName=derived(firstName,lastName,(first,last)=>`${first}${last}`);fullName.value();//=> "John Doe"firstName.set('Jane');fullName.value();//=> "Jane Doe"// Subscribe to be notified of changesconstunsubscribe=fullName.subscribe((nextVal,prevVal)=>{// Do something});If the callback function defines an extra parameter in its signature, the derived store is treated as asynchronous. The callback function is provided a setter for the store's value and no longer relies on the return value:
import{derived,store}from'@ryanmorr/reflex';constquery=store();// Perform an ajax request when the query changes// and notify subscribers with the resultsconstresults=derived(query,(string,set)=>{fetch(`path/to/server/${encodeURIComponent(string)}`).then(set);});Create DOM nodes declaratively via tagged template literals:
import{html}from'@ryanmorr/reflex';// Create an elementconstel=html`<div></div>`;// Create a text nodeconsttext=html`Hello World`;// Create an SVG elementconstrect=html`<rectx="10" y="10" width="100" height="100" />`;// Create a document fragment for multiple root nodesconstfrag=html`<div></div><span></span>`;// Supports attributesconstdiv=html`<divid="foo" class=${'bar'}/>`;// Supports spread attributesconstsection=html`<section...${{id: 'foo',class: 'bar'}}/>`;// Supports styles as an objectconstheader=html`<headerstyle=${{width: '100px',height: '100px'}}/>`;// Supports styles as a stringconstem=html`<emstyle=${'color: red; text-decoration: underline red;'}/>`;// Supports functions for setting child nodesconstheader=html`<header>${(parentElement)=>html`<h1>Title</h1>`}</header>`;// Supports functions for setting attributes (except event listeners)constfooter=html`<footerclass=${(element,attributeName)=>'foo'}></footer>`;// Supports event listeners (indicated by a prefix of "on")constbutton=html`<buttononclick=${(e)=>console.log('clicked!')}>Click Me</button>`;When a reactive store is interpolated into a DOM element created with html, it creates a reactive binding that will automatically update that portion of the DOM, and only that portion, when the internal store value changes:
import{html,store}from'@ryanmorr/reflex';constname=store('John');// Interpolate a store into an elementconstelement=html`<div>My name is ${name}</div>`;// The store value is appended as a text nodeelement.textContent;//=> "My name is John"// The store is bound to the text node, changing// the store value automatically updates the text// node and only that text nodename.set('Jim');// After rendering is completedelement.textContent;//=> "My name is Jim"Similarly to stores, promises can also be interpolated into a DOM element created with html, setting the value of the node/attribute when the promise resolves:
import{html}from'@ryanmorr/reflex';constpromise=Promise.resolve('World');// Interpolate a promise like anything elseconstelement=html`<div>Hello ${promise}</div>`;// After the promise resolves and rendering is completedelement.textContent;//=> "Hello World"Functional components are also supported. Since reflex is not virtual DOM, a component is only executed once, making both stateless and stateful components easy:
import{html,store}from'@ryanmorr/reflex';// A simple component to wrap a common pattern with props and child nodesconstStateless=({id, children})=>{returnhtml`<sectionid=${id}>${children}</section>`;};// Create the component and return a DOM elementconstsection=html`<${Stateless}id="foo">bar<//>`;// A component that holds stateconstStateful=()=>{constgetTime=()=>newDate().toLocaleTimeString();consttime=store(getTime());setInterval(()=>time.set(getTime()),1000);returnhtml`<div>Time: ${time}</div>`;};// Create the stateful component just like a stateless oneconstdiv=html`<${Stateful}/>`;If the component function defines an extra parameter as part of its signature, it is provided a function for registering callbacks to be invoked when the component is mounted to the DOM. Optionally, the mount callback can return a cleanup function that is executed when the component is disposed:
import{html}from'@ryanmorr/reflex';constComponent=(props,mount)=>{mount((element)=>{// Executed when the component is appended to // the DOM and is provided the root element(s)return()=>{// Executed when the component is disposed};});returnhtml`<div></div>`;};When creating elements with html, the ref attribute can be used to invoke a function when the element is first created. This is useful for initializing elements and collecting references to deeply nested elements:
import{html}from'@ryanmorr/reflex';constelement=html`<divref=${el=>/* initialize element */}></div>`;Additionally, assigning a store as the value of a ref attribute will add the element to an internal array within the store. Subscribers of the store will be notified when elements are added and removed:
import{html,store,dispose}from'@ryanmorr/reflex';// Use a store to group multiple element referencesconstfoo=store();constelement=html`<ul><liref=${foo}></li><liref=${foo}></li><liref=${foo}></li><liref=${foo}></li></ul>`;// Returns an array of all elements in the storeconstelements=foo.value();// Subscribe to be called when elements are added or removedfoo.subscribe((nextElements,prevElements)=>{// Do something});// Disposing an element will automatically remove it from the storedispose(element.lastChild);Create a side effect that is executed every time the DOM has been updated and return a function to stop future calls:
import{effect}from'@ryanmorr/reflex';conststop=effect(()=>{// DOM has been updated});Providing one or more dependencies will create a side effect that is guaranteed to execute after a store value changes and any portion of the DOM that depends on that store has been updated:
import{effect,store}from'@ryanmorr/reflex';constid=store('foo');constcontent=store('bar');conststop=effect(id,content,(idVal,contentVal)=>{// Invoked anytime `id` or `content` changes and the DOM has been updated});Create a two-way binding between a store and a form field, allowing the store to be automatically updated with the current value of the form element when the user changes it, and vice-versa. It supports inputs, checkboxes, radio buttons, selects, and textareas:
import{bind,html,store}from'@ryanmorr/reflex';constvalue=store('foo');constelement=html`<inputvalue=${bind(value)}/>`;Alternatively, bind can be used to support stores as event listeners:
import{bind,html,store}from'@ryanmorr/reflex';constclicked=store();constbutton=html`<buttononclick=${bind(clicked)}>Click Me</button>`;clicked.subscribe((event)=>console.log('clicked'));Efficiently diffs and renders lists when provided a reactive store that encapsulates an iterable value. Upon reconciliation, the each function uses a strict equality operator (===) to compare the indexed values of the iterable and determine if an element has been removed or relocated:
import{each,html,store}from'@ryanmorr/reflex';constitems=store([1,2,3,4,5]);constelement=html`<ul>${each(items,(item,index,array)=>html`<li>${index+1}: ${item}</li>`)}</ul>`;Provide a fallback function as an optional third argument to render content when the store contains an empty iterable or non-iterable value:
import{each,html,store}from'@ryanmorr/reflex';constitems=store([]);constelement=html`<section>${each(items,(item)=>html`<div>${item}</div>`,()=>html`<div>No Results</div>`)}</section>`;Reflex uses deferred rendering to batch DOM updates. The tick function returns a promise that is resolved when all previously queued DOM updates have been rendered:
import{tick}from'@ryanmorr/reflex';// Embed a store in the DOMconststore=store('foo');constelement=html`<div>${store}</div>`;// Change a store value to trigger a re-renderstore.set('bar');// The DOM is up-to-date when the `tick` promise resolvesawaittick();Register a callback function to be invoked when an element is disposed. An element is disposed implicitly only during an each DOM reconciliation or explicitly when the dispose function is called on the element or an ancestor element:
import{cleanup}from'@ryanmorr/reflex';cleanup(element,()=>console.log('element and child nodes disposed'));Destroy all node-store bindings to prevent future DOM updates and invoke any registered cleanup functions for an element and its descendants. It will also remove the element and its descendants from any store it was added to via the ref attribute:
import{dispose,store,html,tick}from'@ryanmorr/reflex';// Create an element-store bindingconstfoo=store('foo');constelement=html`<div>${foo}</div>`;// Update elementfoo.set('bar');awaittick();console.log(element.textContent);//=> "bar"// Destroy the element-store bindingdispose(element);// The element is no longer updatedfoo.set('baz');awaittick();console.log(element.textContent);//=> "bar"For a CSS-in-JS solution, refer to fusion, a similar library that brings reactivity to CSS variables, media queries, keyframes, and element queries among other helpers. It is also 100% compatible with reflex.
This project is dedicated to the public domain as described by the Unlicense.