Skip to main content Accessibility Feedback
Versions

Reef v12

A tiny utility library for building reactive state-based UI.

Reef is a simpler alternative to React, Vue, and other UI libraries. No build steps. No fancy syntax. Just vanilla JS and a few small utility functions.

Features

  • Weighs just 1.6kb minified and gzipped, with zero dependencies.
  • Simple templating with JavaScript strings or template literals.
  • Load it with a <script> element or ES module import—no command line or transpiling required (though you can if you want).
  • Uses DOM diffing to update only the things that have changed.
  • Automatically sanitizes HTML before rendering to help protect you from cross-site scripting (XSS) attacks.
  • Write vanilla JS, and use a few small utility methods only when they’re needed.
  • Compatible with all modern browsers.

Make web development fun and simple again!

Installation

Reef works without any build step.

The CDN is the fastest and simplest way to get started, but you can use importable modules or a direct download if you’d prefer.

<!-- Get the latest major version -->
<script src="https://cdn.jsdelivr.net/npm/reefjs@12/dist/reef.min.js"></script>

With the global script, you can call the API methods on the reef object, or destructure them into their own variables.

// You do this...
reef.store();

// or this...
let {store} = reef;

Reef uses semantic versioning. You can grab a major, minor, or patch version from the CDN with the @1.2.3 syntax. You can find all available versions under releases.

More ways to install Reef

ES Modules

Reef also supports modern browsers and module bundlers (like Rollup, Webpack, Snowpack, and so on) using the ES modules import syntax. Use the .es version.

import {store, component} from 'https://cdn.jsdelivr.net/npm/reefjs@12/dist/reef.es.min.js';

NPM

You can also use NPM (or your favorite package manager). First, install with NPM.

npm install reefjs --save

Then import the package.

import {store, component} from 'reefjs';

CommonJS

If you use NodeJS, you can import Reef using the require() method with the .cjs version.

let {store, component} = require('https://cdn.jsdelivr.net/npm/reefjs@12/dist/reef.cjs.min.js');

Direct Download

You can download the files directly from GitHub.

Compiled and production-ready code can be found in the dist directory. The src directory contains development code.

<script src="path/to/reef.min.js"></script>

Quick Start

Reef is a tiny utility library with just three functions: store(), render(), and component().

Create reactive data with the store() method. Pass in an array or object, and Reef will emit a reef:store event whenever a property is updated.

let {store} = reef;

// Create a reactive data store
let data = store({
	greeting: 'Hello',
	name: 'World'
});

// Emits a reef:store event
data.greeting = 'Hi';

Safely render UI from an HTML string with the render() method. Pass in an element or element selector and your HTML string. Reef will sanitize your HTML, then diff the DOM and update only the things that are different.

let {render} = reef;

let name = 'world';
render('#app', `<p>Hello, ${name}!</p>`);

Automatically update your UI when data changes with the component() method. Pass in an element or element selector and a template function. Reef will listen for reef:store events and and run the render() function.

let {store, component} = reef;

// Create a reactive data store
let data = store({
	greeting: 'Hello',
	name: 'World'
});

// Create a template function
function template () {
	let {greeting, name} = data;
	return `<p>${greeting}, ${name}!</p>`;
}

// Create a component
// Renders into the UI, and updates whenever the data changes
component('#app', template);

// The UI will automatically update
data.greeting = 'Hi';
data.name = 'Universe';

Try this demo on CodePen →

Advanced Technique

Reef is a set of small functions you can mix-and-match as needed. As your project gets bigger, the way you manage components and data may need to grow with it.

Default and state-based HTML attributes

You can use data to conditionally include or change the value of HTML attributes in your template.

To dynamically set checked, selected, and value attributes, prefix them with an @ symbol. Use a falsy value when the item should not be checked or selected.

In the example below, the checkbox is checked when agreeToTOS is true.

// The reactive store
let data = store({
	agreeToTOS: true
});

// The template
function template () {
	return `
		<label>
			<input type="checkbox" @checked="${agreeToTOS}">
		</label>`;
}

// The component
component('#app', template);

You might instead want to use a default value when an element initially renders, but defer to any changes the user makes after that.

You can do that by prefixing your attributes with a # symbol.

In this example, Merlin has the [selected] attribute on it when first rendered, but will defer to whatever changes the user makes when diffing and updating the UI.

function template () {
	return `
		<label for="wizards">Who is the best wizard?</label>
		<select>
			<option>Gandalf</option>
			<option #selected>Merlin</option>
			<option>Ursula</option>
		</select>`;
}

Try controlling form attributes on CodePen →

Multiple Stores

With Reef, you can create components that use data from multiple reactive stores.

// Create multiple reactive store
let data = store({
	heading: 'My Todos',
	emoji: 'πŸ‘‹πŸŽ‰'
});
let todos = store(['Swim', 'Climb', 'Jump', 'Play']);

// Create a template
function template () {
	let {heading, emoji} = data;
	return `
		<h1>${heading} ${emoji}</h1>
		<ul>
			${todos.map(function (todo) {
				return `<li id="${todo.toLowerCase().replaceAll(' ', '-')}">${todo}</li>`;
			}).join('')}
		</ul>`;
}

// Create a reactive component
// It automatically renders into the UI
component('#app', template);

If your stores use custom event names, pass them in as an array of store names with the options.stores property.

// Create multiple reactive store
let data = store({
	heading: 'My Todos',
	emoji: 'πŸ‘‹πŸŽ‰'
}, 'heading');
let todos = store(['Swim', 'Climb', 'Jump', 'Play'], 'todos');

// ...

// Create a reactive component with multiple stores
component('#app', template, {stores: ['heading', 'todos']});

Try components with multiple stores on CodePen →

Batch Rendering

With a component(), multiple reactive data updates are often batched into a single render that happens asynchronously.

// Reactive store
let todos = store(['Swim', 'Climb', 'Jump', 'Play']);

// Create a component from a template
component('#app', template);

// These three updates would result in a single render
todos.push('Sleep');
todos.push('Wake up');
todos.push('Repeat');

You can detect when a UI update happens inside a component by listening for the reef:render event.

It’s emitted directly on the element that was rendered, and also bubbles if you want to listen for all render events.

// Log whenever an element is rendered into
document.addEventListener('reef:render', function (event) {
	console.log('The UI was just updated inside this element.');
	console.log(event.target);
});

Try batch rendering on CodePen →

More efficient DOM diffing with IDs

Unique IDs can help Reef more effectively handle UI updates.

For example, imagine you have a list of items, and you’re rendering them into the UI as an unordered list.

// Reactive store
let todos = store(['Swim', 'Climb', 'Jump', 'Play']);

// The template
function template () {
	return `
		<ul>
			${todos.map(function (todo) {
				return `<li>${todo}</li>`;
			})}
		</ul>`;
}

// Create a component
component('#app', template);

The resulting HTML would look like this.

<ul>
	<li>Swim</li>
	<li>Climb</li>
	<li>Jump</li>
	<li>Play</li>
</ul>

Next, let’s imagine that you remove an item from the middle of your array of todos.

// remove "Climb"
todos.splice(1, 1);

Because of how Reef diffs the UI, rather than removing the list item (li) with Climb as it’s text, it would update the text of Climb to Jump, and the text of Jump to Play, and then remove the last list item from the UI.

For larger and more complex UIs, this can be really inefficient.

You can help Reef more effectively diff the UI by assigning unique IDs to elements that may change.

// The template
function template () {
	return `
		<ul>
			${todos.map(function (todo) {
				let id = todo.toLowerCase();
				return `<li id="${id}">${todo}</li>`;
			})}
		</ul>`;
}

Now, the starting HTML looks like this.

<ul>
	<li id="swim">Swim</li>
	<li id="climb">Climb</li>
	<li id="jump">Jump</li>
	<li id="play">Play</li>
</ul>

If you remove Climb from the todos array, Reef will now remove the #climb element rather than updating all of the other list items (and any content within them).

Tip: you can easily generate unique IDs using the crypto.randomUUID() method.

Events

The preferred way to listen to events in a Reef template is event delegation.

Run the addEventListener() method on the element you’re rendering your template into, and filter out events that occur on elements you don’t care about.

// The count
let count = store(0);

// Increase the count by 1 when the [data-count] button is clicked
function increase (event) {
	if (!event.target.matches('[data-count]')) return;
	count.value++;
}

// The template
function template () {
	return `<button data-count>Clicked ${count.value} times</button>`;
}

// Render the component
let app = document.querySelector('#app');
component(app, template);

// Listen for events
app.addEventListener('click', increase);

Try event delegation on CodePen →

By default, on* events on elements are removed when rendering to reduce the risk of XSS attacks.

If you’d prefer to attach events directly to elements in your template using on* events, you must register them by passing an object of named event listener callback functions into your component as the events option.

Under-the-hood, Reef will remove any event handlers that aren’t registered.

let {store, component} = reef;

// The count
let count = store(0);

// Increase the count by 1
function increase () {
	count.value++;
}

// The template
function template () {
	return `<button onclick="increase()">Clicked ${count.value} times</button>`;
}

// Register event listener methods
let events = {increase};

// Render the component
component('#app', template, {events});

Try event binding on CodePen →

Setter Functions

Reef’s store() method makes updating your UI as simple as updating an object property. But as your app scales, you may find that keeping track of what’s updating state and causing changes to the UI becomes harder to track and maintain.

Setter functions provide you with a way to control how data flows in and out of store object. Use the setter() method to create a store that can only be updated with setter functions that you define at time of creation.

Pass in your data and an object of functions as arguments. Setter functions automatically receive the data object as their first argument.

let {setter} = reef;

let todos = setter(['Swim', 'Climb', 'Jump', 'Play'], {

	// Add an item to the todo list
	add (todos, todo) {
		todos.push(todo);
	},

	// Remove a todo item by name
	delete (todos, todo) {
		let index = todos.indexOf(todo);
		if (index < 0) return;
		todos.splice(index, 1);
	}

});

You can update your data by calling one of your setter methods directly on the setter object. Trying to update the data directly will not work.

This protects your component or store data from unwanted changes. The data property always returns an immutable copy.

// This will update the data
todos.add('Take a nap');
todos.delete('Jump');

// This WILL not
todos.push('Do it again tomorrow');

Try setter functions on CodePen →

Reactive data and manual UI updates

If you have a more simple UI component, you can combine the store() method with the browser-native Element.addEventListener() to manually update your UI instead of using the render() function.

For example, imagine you have an element that displays the number of items in a shopping cart.

Cart (<span id="cart-items">0</span>)

Rather than diffing the DOM every time that number of items changes, you can listen for data updates and use the Element.textContent property to manually update the UI. It will be faster and simpler.

// Get the cart element
let cartCount = document.querySelector('#cart-items');

// Create a reactive store
let cart = store([], 'cart');

// Update how many cart items are displayed in the UI
document.addEventListener('reef:store-cart', function () {
	cartCount.textContent = cart.length;
});

// Add an item to the cart
// The UI will automatically be updated
cart.push({
	item: 'T-Shirt',
	size: 'M',
	cost: 29
});

Try manual UI updates on CodePen →

Native Web Components

You can include native web components inside the HTML template strings that get rendered by Reef.

Because web components control their own internal content, Reef will modify element attributes, but will not diff content within them.

// Create a reactive store
let data = store({
	heading: 'My Counter',
	emoji: 'πŸ‘‹πŸŽ‰'
});

// Create a template
function template () {
	let {heading, emoji} = data;
	return `
		<h1>${heading} ${emoji}</h1>
		<count-up></count-up>`;
}

// Reef will NOT diff the content of the count-up element
data.heading = 'Count it';

API Reference

Reef includes just three utility methods and a handful of lifecycle events.

store()

Create a reactive data object.

It accepts any value as an argument. If no value is provided, it uses an empty object by default. If a string or number is used, it returns an object with the value property.

let {store} = reef;

let data = store({
	greeting: 'Hello',
	name: 'World'
});

// returns {value: 42}
let num = store(42);

This emits a reef:store event on the document whenever a property is modified. The event.detail property contains the current value of the data.

// Listen for data changes
document.addEventListener('reef:store', function (event) {
	console.log('The data was updated!');
	console.log(event.detail);
});

// Update the data
data.greeting = 'Hi there';

Try data reactivity on CodePen →

You can customize the event name by passing a second argument into the store() method. It gets added to the end of the reef:store event with a dash delimiter (-).

let wizards = store([], 'wizards');

// A "reef:store-wizards" event gets emitted
wizards.push('Merlin');

Try custom event names on CodePen →

render()

Render an HTML template string into the UI.

Pass in the element (or element selector) to render into, and an HTML string to render.

Unlike the Element.innerHTML property, this sanitizes your HTML to reduce the risk of XSS attacks, and diffs the DOM, only updating the things that have changed.

let {render} = reef;

// Create a template
function template () {
	return '<p>Hello, world!</p>';
}

// Render it into the #app element
render('#app', template());

Try rendering HTML on CodePen →

To reduce the risk of XSS attacks, dangerous properties (including on* events) are removed from the HTML before rendering.

// The onerror event is removed before rendering
render('#app', '<p><img src="x" onerror="alert(1)"></p>');

Try HTML sanitization on CodePen →

If you want to allow on* event listeners, pass an object of named events listener functions into render() function as the third argument.

Any on* events that are not passed into your render() function are removed to reduce the risk of XSS attacks.

let {render} = reef;

// Track clicks
let count = 0;

// Log clicks
function log () {
	count++;
	console.log(`Clicked ${count} times.`);
}

// Warn clicks
// Won't run because it's not registered
function warn () {
	count++;
	console.warn(`Clicked ${count} times.`);
}

// Register event handlers
let events = {log};

// Render a button with an onclick event
render('#app', `<button onclick="log()">Activate Me</button> <button onclick="warn()">This won't work</button>`, {events});

Try event listener binding on CodePen →

Note: this is only needed if you’re using on* events directly on your elements. If you’re using event delegation outside of your template, it’s not needed.

component()

Create a reactive component.

Pass in the element (or element selector) to render into, and a template function that returns an HTML string to render.

The component() method will render it into the UI, and automatically update the UI whenever your reactive data changes.

let {store, component} = reef;

// Create a reactive store
let todos = store(['Swim', 'Climb', 'Jump', 'Play']);

// Create a template
function template () {
	return `
		<ul>
			${todos.map(function (todo) {
				return `<li>${todo}</li>`;
			}).join('')}
		</ul>`;
}

// Create a reactive component
// It automatically renders into the UI
component('#app', template);

// Automatically adds a new list item to the UI
todos.push('Take a nap... zzzz');

Try creating a component on CodePen →

The component() method also accepts an object of options as a third argument.

  • events - An object of allowed event binding callback functions.
  • stores - An array of custom event names to use for store() events.
// Allow registered on* events
component('#app', template, {events: reverseWizards});

// Use a custom event name
let wizards = store([], 'wizards');
component('#app', template, {stores: ['wizards']});

// Use a custom name AND allow register on* events
component('#app', template, {stores: ['wizards'], events: reverseWizards});

Try component options on CodePen →

If you assign your component to a variable, you can stop reactive rendering with the component.stop() method, and start it again with the component.start() method.

The component.render() method manually renders a component in the UI.

// Create a component
let app = component('#app', template);

// Stop reactive rendering
app.stop();

// Restart reactive rendering
app.start();

// Manually render a component
app.render();

Try component methods on CodePen →

setter()

Create a reactive data object that can only be updated with setter functions that you define at time of creation.

It accepts the data as the first argument, and an object of setter functions as the second argument. Setter functions automatically receive the data object as their first argument.

let {setter} = reef;

let todos = setter(['Swim', 'Climb', 'Jump', 'Play'], {

	// Add an item to the todo list
	add (todos, todo) {
		todos.push(todo);
	},

	// Remove a todo item by name
	delete (todos, todo) {
		let index = todos.indexOf(todo);
		if (index < 0) return;
		todos.splice(index, 1);
	}

});

You can update your data by calling one of your setter methods directly on the setter object. Trying to update the data directly will not work.

// This will update the data
todos.add('Take a nap');
todos.delete('Jump');

// This WILL not
todos.push('Do it again tomorrow');

Try setter functions on CodePen →

The setter() method creates a store under-the-hood, and emits a reef:store event on the document whenever a property is modified with a setter function.

You can customize the event name by passing a third argument into the setter() method. It gets added to the end of the reef:store event with a dash delimiter (-).

let todos = setter([], {
	add (todos, todo) {
		todos.push(todo);
	},
}, 'todos');

// A "reef:store-todos" event gets emitted
todos.add('Go to the store');

Lifecycle Events

Reef emits custom events throughout the lifecycle of a component or reactive store.

  • reef:store is emitted when a reactive store is modified. The event.detail property contains the data object.
  • reef:start is emitted on a component element when reef starts listening for reactive data changes.
  • reef:stop is emitted on a component element when reef stops listening for reactive data changes.
  • reef:before-render is emitted on a component element before it renders a UI update. The event.preventDefault() method cancels the render.
  • reef:render is emitted on a component element when reef renders a UI update.

You can listen for Reef events with the Element.addEventListener() method.

// Log whenever an element is rendered into
document.addEventListener('reef:render', function (event) {
	console.log('The UI was just updated inside this element.');
	console.log(event.target);
});

Demos

Want to see Reef in action? Here are some demos and examples you can play with.

Browser Compatibility

Reef works in all modern browsers. That means:

  • The latest versions of Edge, Chrome, Firefox, and Safari.
  • Mobile Safari, Chrome, and Firefox on iOS.
  • WebView, Chrome, and Firefox for Android.

If you need to support older browsers, you’ll need to transpile your code into ES5 with BabelJS.

License

The code is available under the MIT License.