Smarter than React.

Easier than jQuery.

It's also blazing fast, and weighs in at 11KB.

Install

npm install reflex-core reflex-ml

or

<script src="https://...."></script>

Why Reflex Instead of React, Angular, etc?

Usage

Simple Static HTML Hierarchies

document.body.append(
ml.div(
"these", "all", "become", "css", "classes",
ml.section(
ml.h1(ml`Welcome to my app!`),
ml.img({ src: "https://img.icons8.com/ultraviolet/100/000000/running-rabbit.png" }),
ml.p(ml`
Values wrapped in the ml() function, or use
the ml tagged template syntax get interpreted
as text content to be inserted in the containing
element.
`
),
ml.p(ml`Raw strings are interpreted as CSS classes.`)
)
)
);

Interactivity

document.body.append(
ml.div(
ml.h1(ml`Welcome to my app!`),
ml.p(ml`
Hover over this element to stream
random numbers into the containing
<div> element.
`
),
on("mousemove", () => ml(Math.random()))
)
)

How this works: Reflex has a global on() function that you use to connect recurrent functions (event handlers) to your elements. But these aren't like normal event handlers. When a recurrent function is called, it's return value is streamed back as a real-time input to the element that contains it. Recurrent functions can stream back anything that could have originally been passed as a parameter to the ml.div(…) function–text content, HTML elements, CSS classes, even other recurrent functions. For example:

document.body.append(
ml.div(
ml.h1(ml`Welcome to my app!`),
ml.p(ml`
Click this element to begin.
`
),
on("click", () => [
ml.p(ml`
Hover over this element to stream
random numbers in below.
`
),
on("mousemove", () => ml(Math.random()))
])
)
);

Forces

Forces are a core concept in Reflex. Forces are special primitives or functions that cause something else to happen when invoked.

Forces may or may not have state. A stateless force is a function that calls other callbacks that are bound to some element. Stateless forces are created when the force() function is called with no parameters. In this case, it returns a function that just forwards to (potentially many) other functions when called. Forces can be created anywhere and attached to any element:

const greet = force<(name: string) => void>();

// If using raw JavaScript, this would be reduced to:
// const greet = force();

document.body.append(
ml.div(
ml.button(
ml`Greet`,
on("click", () => greet("Paul"))
),
ml.p(
on(greet, name => ml`Hello ${name}.`)
)
)
);

Forces can also be stateful. If you pass a string, number or boolean to the force() function, you get back an object with a boxed value that does it's thing when that value is changed. Here is an example using a boolean:

const flag = force(false);

document.body.append(
ml.div(
// Effects that store a boolean value are given an additional
// method called .flip() to toggle it's internal value:
on("click", () => flag.flip()),
on(flag, () => flag.value ? "narrow" : "wide"),
ml`Click me!`
)
);

Alternatively, you can pass in an array, and use this to synchronize changes between that array and the view:

const list = force([6, 3, 8]);

document.body.append(
ml.div(
ml.button(
ml`Add`,
on("click", () => void list.push(Math.random()))
),
ml.button(
ml`Remove`,
on("click", () => void list.pop())
),
ml.ul(
on(list, num => ml.li(ml(num)))
)
)
);

Binding to an array doesn't require a container element, and doesn't even need to be binding to elements at all. You can just as easily bind an array to a series of Text objects, or even HTML attributes or CSS class names. What's even better is that all this happens without resorting to complex or performance-impaired strategies like virtual DOM or dirty checking. It just mutates the DOM directly.

2-Way Binding

const value = force("Type something.");

ml.div(
ml.button(
ml`Reset`,
on("click", () => value.set("Reset!"))
),
ml.inputText(
ml.bind(value)
),
ml`The value is: ${value}`
);

Other Inputs To Element Constructions

The element construction functions basically take any reasonable value as input. For example:

Functions

Any function passed an element construction function is passed a reference to the element being constructed, and a live array that stores the children that element:

ml.div(
ml.p(),
ml.p(),
(e, children) =>
{
// e refers to the div
// children is an array with 2 empty <p>'s
}
);

Promises

You can pass in a Promise that eventually resolves to some other value, and that value will be streamed in the right place:

ml.div(
ml.p(ml`Before`),
new Promise(resolve =>
{
// Do some async stuff here
resolve(ml.p(ml`In between`));
}),
ml.p(ml`After`)
);

Generators

Generator functions are accepted. Element ordering is as you would expect:

document.body.append(
ml.div(
ml.p(ml`Paragraph 1`),
function*(e, children)
{
// Do some async stuff here
yield ml.p(ml`Paragraph 2`);
// Do some other async stuff
yield ml.p(ml`Paragraph 3`);
},
ml.p(ml`Paragraph 4`)
)
);

Discarded Values

false, null, undefined, NaN and empty arrays are discarded completely. This makes it really easy to do things like:

ml.div(
someBooleanValue && ml.div(...)
);

Extreme Nesting

You're able to pass all of these things with any level of nesting. A generator could be passed that returns a doubly-nested array that has 3 promises, that each return other generators, that … well ... you get it. This is important because it drastically reduces cognitive load when thinking about program organization, specifically component composition. Reflex makes it really easy to divide your application up into your own "components" using the pre-existing features of JavaScript (functions, classes). You don't need to think too much about the format of the return values of these functions, because in the end, Reflex figures out what your intention was.

// Awful code:
ml.div(
[[[() => [[ml.div(), [ml.div(), () => []]]]]]]
);
// But it reduces to:
// <div>
// <div></div>
// <div></div>
// </div>

Future

Below describes the features that are on the roadmap.

List Synchronization With Sorting and Filters

The array that is returned from force([ ... ]) is upgraded with a few features to make it easier to synchronize it with a list of elements (and if you care about old browsers, there are some minor nuances). Most notably, the filter(), sort() methods have an additional overload whereby the first parameter is an reflex that provides control over when the list of elements should be updated.

Filtering

A one or more filters can be applied to the presentation without affecting the backing array.

const positive = force(true);
const array = force([-1, -2, -3, 1, 2, 3]);

ml.div(
ml.button(
ml`Toggle Positive / Negative`,
on("click", () => positive.flip())
),
on(
array.filter(positive, num => positive.value ? num >= 0 : num < 0),
item => ml(` ${item} `)
);
);

Sorting

Sorting works in much the same way as filtering, using a syntax that is very similar to the familiar Array.sort, with the difference that the special overload returns a sorted copy of the array rather than modifying the array in-place:

const asc = force(true);
const array = force([0, 1, 2, 3, 4, 5, 6]);

ml.div(
ml.button(
ml`Flip sorting`,
on("click", () => asc.flip())
),
on(
array.sort(asc, (a, b) => asc.value ? a - b : b - a),
item => ml(` ${item} `)
);
);

Chaining Multiple Transformations

As you might suspect, you're able to chain as many of these transformations together as necessary (although don't do this needlessly, each transformation consumes resources).

const positive = force(true);
const asc = force(true);
const array = force([-1, -2, -3, 1, 2, 3]);

ml.div(
ml.button(
ml`Toggle Positive / Negative`,
on("click", () => positive.flip())
),
ml.button(
ml`Flip sorting`,
on("click", () => asc.flip())
),
on(
array
.filter(positive, num => positive.value ? num >= 0 : num < 0)
.sort(asc, (a, b) => asc.value ? a - b : b - a),
item => ml(` ${item} `)
);
);