mrr
A funtional reactive programming library for React inspired by Firera. Calculate React state fields as computable cells using pure functions.
QuickStart
;; $init: a: '' b: '' c: { const num = Numbern + Numberm; return ? '' : num; } 'a' 'b' <div> <input value= statea onChange= /> + <input value= stateb onChange= /> = <input value= statec disabled/> </div>;
Mrr adds reactivity to React state: it listens to changes in state(in this case, "a" and "b" properties) and computes dependent properties(like "c" in the example above). The essence of mrr is in these lines:
c: { const num = Numbern + Numberm; return ? '' : num;} 'a' 'b'
Here "c" is computable property - a "cell", "a" and "b" are "arguments" - parent properties, which are passes as arguments to the "formula" - function (n, m) => ... . You may add any number of dependent(computable) properties, each dependent cell may have any number of arguments(more than 0).
Using hooks
The same can be done using hooks:
;; const myComponent = { const state $ = ; return <div> <input value= statea onChange= /> + <input value= stateb onChange= /> = <input value= statec disabled/> </div>; };
Asynchronous computing
In most cases, you should use pure functions for calculating computable properties' values and avoid side-effect. But sometimes you may need to make something asynchronous, e.g. ajax request. For this case, use "async" type. When using 'async' type, the first parameter in function is always callback function, which you should call to return the value for computed property(cell).
In $init section we can optionally set initial values for our computed properties(to make our code work before the data is loaded).
A cell may also depend on other cell.
;
A "type" in mrr means different way of calculating the value of cell. Unlike the Rx approach wich hundred of operators, mrr has only basic 5 types which cover all the cases, and some syntactic sugar.
Special cells
$start
Sometimes we need to do something only once, when the component in created. E.g., load the list of goods in the example above. In this case we can use "$start" cell.
all_goods: 'async' { } 'selectedCategory' '$start'
Now our 'all_goods' cell will be computed when the "selectedCategory" cell changes and when the component is created. (as the very value of "$start" cell is useless for us, we don't mention it in our arguments' list)
$end
Opposite to $start. Runs on componentWillUnmount.
$props
This cell is used when you want to access component's props as a mrr cell.
fullName: { return name + ' ' + propssurname; } 'name' '$props'
Due to new React API(introduces in React 16.3.0), mrr cannot detect and handle property changes, so "$props" cell will not be fired when some property changes. That's why "$props" is always a passive cell, see "Passive listening" below.
Nested type
Nested type allows us to put the results of calculation into different "cells". In general it reminds async type - it also receives callback as first argument. The difference is the callback function receives the name of sub-cell as the first argument, and it's value as second.
;
Here "all_goods" cell was actually into two "sub-" properties: "loading" and "data". We update the value of these properties by calling cb(%subcell%, %value%). They become accessible to the outer world by the name %cell% . %subcell%, e.g. "all_goods.loading".
Funnel type
Say we need to display the popup when user clicks "open popup", and close it when he clicks close or "cancel" button.
;
When using funnel type, your formula function receives only the name and value of cell which changed at that moment. This pattern is very common, and mrr has syntactic sugar for this called "merge":
popup_shown: 'merge' 'open_popup': true 'close_popup': false 'cancel':
Actually, "merge" is not another mrr cell type, it's an operator. See "Creating operators" section for more info.
Closure type
"closure" type allows us to save some data between the formula calls, with the help of... closure.
;
When using closure type, we provide a function, that will return another function, which will be used as actual formula. It has an access to local variables of first function. This is a way to store some data between formula calls safely, without exposing it.
Combining types
There are 5 cell types in mrr: async, nested, funnel, closure, and the default type(used when you don't specify any type). The fact is you can combine them as you like!
// you can combine two or more types by joining them with '.', order is not importantnum: 'closure.funnel' { let num = 0; return { ifcell === 'add' num += val; ifcell === 'subtract' num -= val; return num; }} 'add' 'subtract'
The only exception is you cannot combine "async" and "nested" type, as the second actually includes the first.
Intercomponent communication
// Todos Component ; // AddTodoForm Component ;
To subscribe to changes in child component, you should add the mrrConnect property with a value of mrrConnect(%connect_as%) In this case, we connected AddTodoForm as 'add_todo'.
todos: { arr; return arr; } '^' 'add_todo/new_todo'
We are listening to changes in "new_todo" stream in child which was connected as "add_todo". '^' means the previous value of the very cell, in this case an array of todos.
Passive listening
In fact, this example won't work as expected. Our "new_todo" cell of AddTodoForm will be computed each times "text" or "submit" are changed, so that new todo will be created after you enter first character. That happens because we are subscribed to "text" and emit new todo objects each time the "text" changes. To fix this, we can use passive listening approach.
new_todo: text '-text' 'submit'
We added a "-" before the name of "text" argument. It means that our cell will not be recalculated when the "text" changes. Still it remains accessible in the list of our arguments. Passive listening allows flexible control over the properties calculation.
"skip" value
Each time a cell is updated, mrr recalculates it's dependent cells. However, in many cases we need a way to cancel further updates. To do this, you should return special "skip" value from your formula.
; odd: a % 2 ? a : skip 'numbers' odd_numbers: 'accum' 'odd'
This value is helpful for all kinds of filtering. It's internally used in such operators as "skipSame", "trigger", "transist" etc.
Operators
Operators(also referred as macros) are functions which transform arbitrary expression into valid mrr expression(one of 5 types). Using operator can make your code more expressive and robust. E.g., "merge" operator transforms given object to funnel type.
popup_shown: 'merge' 'open_popup': true 'close_popup': false 'cancel': false
is transformed by operator in compile time to something like this:
popup_shown: 'funnel' { if... if... } 'open_popup' 'close_popup' 'cancel'
There are a number of built-in operator, like "merge", "split", "trigger", "skipN" etc. Each operator should have unique name and should be a function, which takes some expression and returns valid mrr expression. You can also add your custom operator by defining __mrrCustomMacros property of your component.
; ;
Now we can do this code
all_goods: 'async' { } 'selectedCategory' '$start'
slightly more beautiful
all_goods: 'promise' 'selectedCategory' '$start'
Linking
Linking streams between components is a crucial part of mrr. You can listen to streams from other components: children or parent. There are number of ways to do it.
// component A foo: { ... } 'my_child/bar' { <div> <B ... /> </div>} // component B bar: { ... } '$start'
When you connect any child component, you should point it's name. Than you can refer to it's streams as "%child_component_name%/%child_component_stream%". There is also an option for listening for all children:
// component A foo: { ... } '*/bar' { <div> <B ... /> <B ... /> <B ... /> </div>} // component B bar: { ... } '$start'
from parent using "../":
// component A foo: { ... } '$start' { <div> <A ... /> </div>} // component B bar: { ... } '../foo'
You cannot use more than one slash in name, i.e. "foo/bar/baz" or "../a/b" are invalid names.
Another way is to link with "connectAs()" function.
// component A foo: { ... } 'bar' { <div> <B ... /> </div>} // component B bar: { ... } '$start'
We pass an array to "connectAs" function, which describes the matching between parent and child streams. We also may use object, if the names are different:
// component A foo: { ... } 'baz' { <div> <B ... /> </div>} // component B bar: { ... } '$start'
We also may link streams in the opposite direction: from parent to child, passing the third argument to "connectAs":
// component A foo: { ... } 'baz' a: { ... } '$start' { <div> <B ... /> </div>} // component B bar: { ... } '$start' b: { ... } 'a'
Error handling
Sometimes errors might appear within the calculation
a: 42 '$start' b: 'a' // TypeError: a is not a function
You can handle them by subscribing to special cell of $err.%cellname%, where cellname is a name of cell where error happened.
a: 42 '$start' b: 'a' showError: 'Some error happened: ' + emessage '$err.b'
If there are no subscribers to special $err.%cellname% cell, the error will be put into general $err cell
a: 42 '$start' b: 'a' c: a 'a' // all errors will be put into $err cell showError: 'Some error happened: ' + emessage '$err'
Debugging
// Now all cells' changes will be logged $log: true $init: ... foo: ... bar: ... baz: ...
$log: // only "foo" and "bar" cells will be logged cells: 'foo' 'bar' // will show the chain of parent cells' changes for each cell showStack: true $init: ... foo: ... bar: ... baz: ...
Reliability
As mrr uses strings as variables, it's easy to make a typo and break the code.
bar: { ... } 'some_click' foo: { ... } 'baz' // a typo, "foo" will never be computed
To fight this, you should specify the list of streams which are read from DOM. Thereby mrr would be able to analyze your code and find broken linking. It's done with special $readFromDOM field.
$readFromDOM: 'some_click' 'another_click' ... bar: { ... } 'some_click' foo: { ... } 'baz' // throws error
Now mrr knows that "baz" is not read from DOM and it's not defined as a stream also, so it's probably a typo, and throws an error.
Basic types: closure, funnel, nested, async
closure
Runs a function on component init, which should return another function used as formula.
vals: 'closure' { // this function would be run once the component is initialized const vals = ; return { // this function would become a formula vals; return vals; }} 'some_cell'
funnel
Allows to listen to each stream independently, receiving the name of stream and it's value when it changes.
popup_state: 'funnel' { ifcell === 'open_popup' return true; else return false; } 'open_popup' 'close_popup' 'close_everything'
async
Allows to run async computations. To yield a result, call a callback function always passed as a first argument to a formula.
user_data: 'async' { ;} 'user_id'
timer: 'async' { ;} '$start'
nested
Allows to "put" the results of computation into different subcells. Callback function is always passed as a first argument to a formula. It accepts the name of subcell you want to put in and the value, or the object where keys/values represent names of subcells and their values.
user_req: 'nested' { ; ;} 'user_id' // use subcells just like any other cell error: 'merge' 'some_error' 'user_req.error'user_data: { ... } 'user_req.data'show_spinner: 'user_req.loading'
Combining basic types
These four basic types(nested, funnel, closure, async) can be used together. You may mix two of them, separated by dot.
// perform request for each element in a stream, but limited to 5 requests simultaneously.req: 'async.closure' { const queue = ; let counter = 0; const max_requests = 5; const make_request = ; return { ifcounter < max_requests ++counter; make_request else queue; }} 'data'
Even three:
timer: 'async.closure.funnel' { let timer; return { ifcell === 'start_timer' timer = ; ifcell === 'stop_timer' ; }} 'start_timer' 'stop_timer'
The only two types you cannot combine together are "async" and "nested", as the second includes the first by design. Combination is possible for these basic types, macros(operators) cannot be combined.
Simple mrr wrapper
allows to use mrr grids outside react: e.g. in vanilla JS projects or for testing.
import { simpleWrapper } from 'mrr';
const grid = simpleWrapper({
$init: {
a: '',
b: '',
},
d: [(n, m, k) => {
const num = Number(n) + Number(m) + (k || 0);
return isNaN(num) ? '' : num;
}, 'a', 'b', 'c']
});
// put some value to a cell
grid.set('a', 10);
// get cell value
grid.get('d'); // ''
grid.set('b', 20);
grid.get('d'); // 30
// listen to cell changes
grid.onChange('d', (val) => console.log('Now d = ' + val));
grid.set('b', 32); // 'Now d = 42'
// and even connect other children!
const child = grid.connect({
val: [() => 58, '$start'],
}, 'foo', { c: val });
// 'Now d = 100'
Advanced system cells
$state
Return the current mrr state - i.e. the object containing all cells values.
;; const myComponent = { const state $ = ; return <div> <input value= statea onChange= /> + <input value= stateb onChange= /> = <input value= statec disabled/> </div>; };
$state is passive by default(see "passive listening" section). It means, you need to subscribe to at least one more cell along with "$state".
$name
Return the name of the grid if it's connected as a child to another grid, otherwise(for root grids) returns undefined. Passive.
const Foo = { const state $ = ; return <div> My name is stategridName </div>;}; // <Foo ... /> // outputs "My name is child1"
$async
Adds callback for returning a value, as if "async" type is used.
goods: 'async' { } 'selectedCategory'
is equal to
goods: { } '$async' 'selectedCategory'
It can be placed in any order:
// the same goods: { } 'selectedCategory' '$async'
$nested
Adds callback for returning a value to a subcell, as if "nested" type is used.
all_goods: 'nested' { ; } 'selectedCategory'
is equal to
all_goods: { ; } '$nested' 'selectedCategory'
$changedCellName
Returns the name of the exact parent cell, which caused the recalculation. (Similar to "funnel" type).
; const foo = ; foo;foo; // true foo;foo; // false
Built-in operators
merge
Joins the streams
quinter: 'merge' 'ene' 'bene' 'raba'
ene: ----------------------------3------------
bene: --"1"--------------false-----------------
raba: ----------"bar"------------------false---
quinter: --"1"-----"bar"----false----3----false---
toggle
Set the value to true when the first argument is fired, and false when the second is fired.
ene: 'toggle' 'bene' 'raba'
bene: --"1"--------------false-------------
raba: ----------"bar"--------------false---
ene: --true----false----true------false---
debounce
foo: 'debounce' 300 'str'
(ms): 0====100====200====300====400====500====600=====700=====
str: ==="a"==="ab"==="aba"======"abab"==="ababa"==="ababag"==
foo: =================="aba"========================"ababag"=
transist
Returns the value of the second argument if the first is truthy and skips if falsy.
make_coffee: 'transist' 'power_enabled' 'start_making_coffee'
start_making_coffee:======="1"====="2"====="3"====="4"====="5"====
power_enabled: =false=============true================false==
make_coffee: ==================="2"="3"====="4"============
&&
Equals to
a && b && ...
Accepts any number of arguments.
a: '&&' 'foo' 'bar' 'baz'
foo: =1===========false===true=========
bar: =2===null=====================10===
baz: =3=================================
a: =3===null=====false===null====3====
||
Similar to "&&"
trigger
Fires true when the argument stream receives certain value.
isLucky: 'trigger' 'val' 13
val: ====10====11====12====13=====14=====15===
isLucky: ======================true===============
skipSame
foo: 'skipSame' 'bar'
bar: ==1=====2=====2====3=====2=====1====="1"==="1"===
foo: ==1=====2==========3=====2=====1====="1"=========
turnsFromTo
Fires when the value of the argument changes from "a" to "b"
foo: 'turnsFromTo' 5 6 'val'
val: ==1===2===3===4===5===6===7===6===5===4===
foo: ======================true================
skipN
Omit first n signals
foo: 'skipN' 'bar' 3
bar: =="a"==="b"==="c"==="d"==="e"==="f"==="g"===
foo: ===================="d"==="e"==="f"==="g"===
accum
Accumulates the values of the stream into array
foo: 'accum' 'bar'
bar: =1======2========3========4===========4=============
foo: =[1]====[1,2]===[1,2,3]===[1,2,3,4]===[1,2,3,4,4]===
Possibly accepts the third argument - an interval(in ms) a value will be stored
foo: 'accum' 'bar' 200
ms: =0========100========200========300========400=========500====
bar: =1======2===========================5=========================
foo: =[1]====[1,2]========[2]======[]====[5]====================[]=
remember
Stores the value of input cell for specified time. May accept an optional default value.
foo: 'remember' 'bar' 100/* time in ms*/ 'N/A'/* default value */
bar: =1==================3=====4==========================5=====
foo: =1==========N/A=====3=====4==========N/A=============5=====
Moving away from strings
Using string constants is a good idea to make your coding proccess more relaxed.
a: 42 '$start' b: a + 10 'a' c: 'promise' 'a' d: 'merge' 'c' 'b' '-a' foo: a + 1 '*/bar'
is transformed into
;;; const a$ = ;const b$ = ;const c$ = ;const d$ = ;const foo$ = ; const bar$ = ; a$: 42 $start$ b$: a + 10 a$ c$: d$: foo$: a + 1
You should create a cell name constant with cell() function. Each operator has it's functional match, imported from 'mrr/operators'. System cells, such as "$start", "$end", "$changedCellName" are imported from 'mrr/cell'. The same goes for passive listening.