Skip to main content Accessibility Feedback
Versions

Reef v11

A lightweight library for creating reactive, state-based components and UI. Reef is a simpler alternative to React, Vue, and other large frameworks.

Features

  • Weighs just 3kb 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.
  • Has Redux/Vuex-like data stores, with setters baked right in.
  • Automatically sanitizes HTML before rendering to help protect you from cross-site scripting (XSS) attacks.
  • Work with native JavaScript methods and browser APIs instead of custom methods and pseudo-languages.
  • Compatible with all modern browsers.

Ditch that bloated framework, and make web development fun and simple again!

Why use Reef?

Reef is an anti-framework.

It does a lot less than the big guys like React and Vue. It doesn’t have a Virtual DOM. It doesn’t require you to learn a custom templating syntax. It doesn’t provide a bunch of custom methods.

Reef does just one thing: render UI.

Couldn’t you just use some template strings and innerHTML? Sure. But Reef only updates things that have changed instead of clobbering the DOM and removing focus from your form fields. It also automatically renders a new UI when your data updates, and helps protect you from XSS attacks.

If you’re craving a simpler, back-to-basics web development experience, Reef is for you.

(And if not, that’s cool too! Carry on.)

Getting Started

1. Include Reef on your site

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@11/dist/reef.min.js"></script>

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 Reef from 'https://cdn.jsdelivr.net/npm/reefjs@11/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 Reef from 'reefjs';

CommonJS

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

let Reef = require('https://cdn.jsdelivr.net/npm/reefjs@11/dist/reef.cjs.min.js');

AMD

If you use RequireJS, SystemJS, and other AMD formats, you can import Reef with the .amd version.

requirejs(['https://cdn.jsdelivr.net/npm/reefjs@11/dist/reef.amd.min.js'], function (Reef) {
  //...
});

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>

2. Add an element to render your component/UI into

This is typically an empty div with a targetable selector.

<div id="app"></div>

3. Create your component

Create a new Reef() instance, passing in two arguments: your selector, and your options.

Provide a selector

The first argument is the selector for the element you want to render the UI into. Alternatively, you can pass in the element itself.

// This works
let app = new Reef('#app');

// This does too
let elem = document.querySelector('#app');
let app = new Reef(elem);

Provide a Template

The second argument is an object of options. It requires a template property, a function that returns an HTML string to render into the DOM.

let app = new Reef('#app', {
	template: function () {
		return '<h1>Hello, world!</h1>';
	}
});

[Optional] Add State/Data

As an optional property of the options argument, you can include state for your component with the data property.

An immutable copy of the data object is automatically passed into your template function, so that you can use it to customize your template.

// Some data
let app = new Reef('#app', {
	data: {
		greeting: 'Hello',
		name: 'world'
	},
	template: function (props) {
		return `<h1>${props.greeting}, ${props.name}!</h1>`;
	}
});

Template literals give you a simple, JSX-like templating experience.

4. Render your component

Render your component by calling the render() method on it.

app.render();

Try the demo on CodePen →

State Management

Reef uses data reactivity to update your UI.

Data reactivity means that the UI “reacts” to changes in your data. Update your data, and the UI automatically renders any required updates based on the new state.

// Create a component
let app = new Reef('#app', {
	data: {
		greeting: 'Hello',
		name: 'world'
	},
	template: function (props) {
		return `<h1>${props.greeting}, ${props.name}!</h1>`;
	}
});

// Render the initial UI
app.render();

// This causes component to update with "Hi, universe"
app.data.greeting = 'Hi';
app.data.name = 'Universe';

You can also update the entire data object.

// This will also update the UI
app.data = {
	greeting: 'Hi',
	name: 'Universe'
};

Try data reactivity on CodePen →

For better performance, multiple property updates may be batched into a single, asynchronous render. You can detect when a render has been completed using the reef:render event.

Non-Reactive Data

Sometimes, you want to update data without updating the UI.

You can get an immutable copy of your data with the Reef.prototype.dataCopy property. This returns an immutable, non-reactive copy of your data that won’t affect the state of your component or cause a UI update.

// Get an immutable copy of the app.data
let data = app.dataCopy;

// Update the copy
// This does NOT update the app.data or render a new UI
data.name = 'Universe';

When you’re ready to update your component data, you can set the component’s data property to your cloned copy.

// Reactively update the component data
app.data = data;

Try non-reactive data on CodePen →

Advanced Components

As your project gets bigger, the way you manage components and data may need to grow with it. Reef has some options to help make things a little easier.

HTML Templates

Default and state-based HTML attributes

You can use component 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.

let app = new Reef('#app', {
	data: {
		agreeToTOS: true
	},
	template: function (props) {
		return `
			<label>
				<input type="checkbox" @checked="${agreeToTOS}">
			</label>`;
	}
});

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, Hermione has the [selected] attribute on it when first rendered, but will defer to whatever changes the user makes when diffing and updating the UI.

let app = new Reef('#app', {
	template: function () {
		return `
			<label for="wizards">Who is the best wizard?</label>
			<select>
				<option>Harry</option>
				<option #selected>Hermione</option>
				<option>Neville</option>
			</select>`;
	}
});

Try controlling form attributes on CodePen →

Preventing Cross-Site Scripting (XSS) Attacks

To reduce your risk of cross-site scripting (XSS) attacks, Reef automatically sanitizes the HTML from your template before rendering it.

In the example below, the attempted XSS attack (the alert()) will not run. Safe HTML, like the bold in the greeting property, will be rendered as expected.

let app = new Reef('#app', {
	data: {
		greeting: '<strong>Hello</strong>',
		name: 'world',
		img: '<img src="x" onerror="alert(1)">'
	},
	template: function (props) {
		return `
			<h1>${props.greeting}, ${props.name}!</h1>
			${props.img}`;
	}
});

Try HTML sanitization on CodePen →

Attaching event listeners to elements

To use event listeners in your templates, allowed callback functions need to be added to the listeners property on the component options object.

Inside the callback function, this refers to the components and its properties. Reef uses event delegation under-the-hood for better performance, and automatically cleans up listeners when the elements they’re attached to are removed.

Any on* callback function not included in the listeners object is removed to reduce the risk of XSS attacks.

new Reef('#app', {
	data: {
		text: ''
	},
	template: function (props) {
		return `
			<label for="mirror">Whatever you type shows up below the field</label>
			<input type="text" oninput="mirror()" id="mirror">
			<div><em aria-live="polite">${props.text.length ? props.text : 'Type something above to change this text'}</em></div>`;
	},
	listeners: {
		mirror: function (event) {
			this.data.text = event.target.value;
		}
	}
}).render();

Try working with event listeners on CodePen →

Getting the element the template is being rendered into

An optional second argument is passed into the template() function: the element that the template is being rendered into.

This is particularly handy if you have data attributes on your element that affect what’s rendered into the template.

<div id="app" data-greeting="Hello"></div>
let app = new Reef('#app', {
	data: {
		name: 'world'
	},
	template: function (props, elem) {
		return `<h1>${elem.getAttribute('data-greeting')}, ${props.name}!</h1>`;
	}
});

Try getting the HTML element that the template was rendered into on CodePen →

Nested Components

If you’re managing a bigger app, you may have components nested inside other components.

You can Reef components inside each other using the Reef.prototype.html() method in your template string. When the parent component is updated, it will automatically update the UI of its nested components if needed.

// Parent component
let app = new Reef('#app', {
	data: {
		heading: 'My Todos'
	},
	template: function (props) {
		return `
			<h1>${props.heading}</h1>
			<div id="todos">
				${todos.html()}
			</div>`;
	}
});

// Nested component
let todos = new Reef('#todos', {
	data: {
		todos: ['Swim', 'Climb', 'Jump', 'Play']
	},
	template: function (props) {
		return `
			<ul>
				${props.todos.map(function (todo) {
					return `<li>${todo}</li>`;
				}).join('')}
			</ul>`;
	}
});

// Render your app
// todos will be automatically rendered because they're nested in the app component
app.render();

Try nested components on CodePen →

Shared State with Data Stores

A Data Store is a special Reef object that holds reactive data you can share with multiple components.

Any time you update the data in your Data Store, any components that use the data will also be updated, and will render again if there are any UI changes.

Create a Data Store using the new Reef.Store() constructor.

let store = new Reef.Store({
	data: {
		heading: 'My Todos',
		todos: ['Swim', 'Climb', 'Jump', 'Play']
	}
});

To use your Data Store with a component, pass it in with the store property instead of providing a data object.

let app = new Reef('#app', {
	store: store,
	template: function (props) {
		return `
			<h1>${props.heading}</h1>
			<ul>
				${props.todos.map(function (todo) {
					return `<li>${todo}</li>`;
				}).join('')}
			</ul>`;

	}
});

When using a Data Store, your component can still have its own local data as well.

The local component data is merged into the store and passed into the template() function as a single props object.

let store = new Reef.Store({
	data: {
		todos: ['Swim', 'Climb', 'Jump', 'Play']
	}
});

let app = new Reef('#app', {
	store: store,
	data: {
		heading: 'My Todos'
	},
	template: function (props) {
		return `
			<h1>${props.heading}</h1>
			<ul>
				${props.todos.map(function (todo) {
					return `<li>${todo}</li>`;
				}).join('')}
			</ul>`;

	}
});

Try creating a Data Store on CodePen →

Note: if any properties in your store and data that share the same name, the local component data gets priority.

Setter Functions

Reef’s reactive data 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 your component.

Add your setter functions to the setters property on your options object. The first parameter on a setter function must be the store or component data. You can add as many other parameters as you’d like.

let store = new Reef.Store({
	data: {
		heading: 'My Todos',
		todos: ['Swim', 'Climb', 'Jump', 'Play']
	},
	setters: {
		// Add a new todo item to the component
		addTodo: function (props, todo) {
			props.todos.push(todo);
		}
	}
});

Use setter functions by calling the do() method on your component or store. Pass in the name of the setter, along with any required arguments (except for props).

// Add a new todo item
store.do('addTodo', 'Take a nap... zzzz');

When a component or store has setter functions, they become the only way to update app or store data.

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

// This will NOT update the store.data or the UI
store.data.todos.push('Take a nap... zzzz');

Try working with setter functions on CodePen →

Asynchronous Data

You can use asynchronous data (such as content from an API) in your templates.

Set an initial default value, make your API call, and update the data property once you get data back. This will automatically trigger a render.

// Create an app
let app = new Reef('#app', {
	data: {
		articles: []
	},
	template: function (props) {

		// If there are no articles
		if (!props.articles.length) {
			return `<p>There are no articles.</p>`;
		}

		// Otherwise, show the articles
	return `
		<ul>
			${props.articles.map(function (article) {
				return `<li>
					<strong><a href="#">${article.title}.</a></strong>
					${article.body}
				</li>`;
			}).join('')}
		</ul>`;
	}
});

// Fetch API data
// Then, update the app data
fetch('https://jsonplaceholder.typicode.com/posts').then(function (response) {
	return response.json();
}).then(function (data) {
	app.data.articles = data;
});

Try create a template from asynchronous data on CodePen →

You might also choose to hard-code a loading message in your markup.

<div id="app">Loading...</div>

Debugging

By default, Reef fails silently. You can put Reef into debug mode to expose helpful error message in the Console tab of your browser’s Developer Tools.

Turn debug mode on or off with the Reef.debug() method. Pass in true to turn it on, and false to turn it off.

// Turns debug mode on
Reef.debug(true);

// Turns debug mode off
Reef.debug(false);

API Reference

Reef provides various options, methods, and custom events that you can hook into.

Global Methods

Run these methods directly on the Reef object.

Reef.debug()

Turn debug mode on or off. Pass in true to turn debug mode on, and false to turn it off.

// Turn debug mode on
Reef.debug(true);

// Turn debug mode off
Reef.debug(false);

Component Properties

Access these properties on individual Reef components.

Reef.prototype.data

Get a reactive copy of the app data.

let data = app.data;

Reef.prototype.dataCopy

Get a non-reactive, immutable copy of the app data.

let copy = app.dataCopy;

// This will not update the component data or cause a render
copy.todos.push('Zzzz... take a nap!');

Reef.prototype.elem

The element the component is associated with. Returns a string or Node.

let elem = app.elem;

Component Methods

Run these methods on individual Reef components.

Reef.prototype.render()

Render a Reef component in the UI.

let app = new Reef('#app', {
	template: function () {
		return 'Hello world!';
	}
});

app.render();

Reef.prototype.html()

Get the compiled HTML string for a component. Used for nesting components.

let app = new Reef('#app', {
	template: function () {
		return 'Hello world!';
	}
});

app.html();

Reef.prototype.do()

Run a setter function. Pass in the name of the setter, and a comma-separate list of any arguments.

let app = new Reef('#app', {
	data: {
		count: 0
	},
	template: function (props) {
		return count;
	},
	setters: {
		increase: function (props) {
			props.count++;
		}
	}
});

// Run the increase setter
app.do('increase');

Events

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

All Reef events follow a reef:{event-name} pattern. Unless otherwise specified, all events are emitted on the document element. Event details can be accessed on the event.detail property.

// Listen for when Reef components are rendered into the DOM
document.addEventListener('reef:render', function (event) {
	console.log(event.target); // The element it was rendered into
	console.log(event.detail); // The component, and the data used for the render
});
  • reef:ready is emitted when Reef is loaded into the DOM and ready to use.
  • reef:initialized is emitted when a new Reef component is initialized.
    • event.detail - the instance
  • reef:before-render is emitted on an element before a new UI is rendered into it.
    • event.target - the element the component is being rendered into
    • event.detail - the component and the data at time of render
    • event.preventDefault() - stop the component from rendering
  • reef:render is emitted on an element after a new UI is rendered into it.
    • event.target - the element the component was rendered into
    • event.detail - the component and the data that was used for the render

Options

All of the options for Reef.

// This can be a string or a element
let elem = '#app';

new Reef(elem, {

	// The component data
	data: {},

	// A data store to use
	store: null,

	// An object of setter methods
	setters: {},

	// An object of allowed event listeners
	listeners: {}

});

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 Safari.
  • 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.