Reactive functions provide a simple way to update a computed value whenever one or more objects change. While model events respond to specific model methods and path patterns, reactive functions will be re-evaluated whenever any of their inputs or nested properties change in any way.
Reactive functions may be run any number of times, so they should be pure functions. In other words, they should always return the same results given the same input arguments, and they should be side effect free. By default, the inputs to the function are retrieved directly from the model, so be sure not to modify any object or array input arguments. For example, slice an array input before you sort it. The output of the model function is deep cloned by default.
To execute a model function, you then call model.start()
or model.evaluate()
.
evaluate()
runs a function once and returns the result.start()
also sets up event listeners that continually re-evaluate thevalue = model.start(path, inputPaths, [options], fn) value = model.evaluate(inputPaths, [options], fn)
// Legacy (racer <= 0.9.5) value = model.start(path, inputPaths..., [options], fn) value = model.evaluate(inputPaths..., [options], fn)
path
- string | ChildModel - The output path at which to set the value, keeping it updated as input paths changeinputPaths
- Array<string | ChildModel> - One or more paths whose values will be retrieved from the model and passed to the function as inputsoptions
- Object (optional)
copy
- Controls automatic deep copying of the inputs and output of the function. Model#evaluate never deep-copies output, since the return value is not set onto the model.
'output'
(default) - Deep-copy the return value of the function'input'
- Deep-copy the inputs to the function'both'
- Deep-copy both inputs and output'none'
- Do not automatically copy anythingmode
- Themodel.set*
method to use when setting the output. This has no effect in Model#evaluate.
'diffDeep'
(default) - Do a recursive deep-equal comparison on old and new output values, attempting to issue fine-grained ops on subpaths where possible.'diff
- Do an identity comparison (===
) on the output value, and do a simple set if old and new outputs are different.'arrayDeep'
- Compare old and new arrays item-by-item using a deep-equal comparison for each item, issuing top-level array insert, remove, and move ops as needed. Unlike'diffDeep'
, this will not issue ops deep inside array items.'array'
- Compare old and new arrays item-by-item using identity comparison (===
) for each item, issuing top-level array insert, remove, and move ops as needed.async
- boolean - If true, then upon input changes, defer evaluation of the function to the next tick, instead of immediately evaluating the function upon each input change. Introduced in racer@0.9.5.
- This can improve UI performance when multiple inputs to a reactive function will change in the same event loop, as
async: true
will mean the function only needs be evaluated once instead of N times.- Warning: Avoid using
async: true
if there's any controller code that does amodel.get()
on the output path or any paths downstream of the output, since changes to an input path won't immediately result in the output being updated.fn
- Function | string - A function or the name of a function defined viamodel.fn()
- The function gets invoked with the values at the input paths, one input per argument, and should return the computed output value.
- It should be a synchronous pure function.
- One common side effect to avoid is
Array#sort
on an input array, since that sorts the array in-place. If you need to do a sort, make a shallow copy viaarray.slice()
first, or use a sorting library that returns a new array instead of sorting in-place.- The function will be called both in Node and in the browser, so avoid using functions whose behavior is implementation-dependent, such as the one-argument form of
String#localeCompare
.- The function might get called with some inputs
undefined
, so be defensive and check inputs' existence before using them.- Return
value
- The initial value computed by the function
model.stop(path)
path
The path at which the output should no longer update. Note that the value is not deleted; it is just no longer updated
In DerbyJS, model.start()
functions should typically be established in the init
method of a component. This method is called both before rendering on the server and then again before rendering in the browser. These reactive functions will be stopped as soon as the component is destroyed, which happens automatically when the component is removed from the page.
MyComponent.prototype.init = function(model) {
model.start('total', 'first', 'second', function sum(x, y) {
return (x || 0) + (y || 0);
});
};
Most reactive functions define a getter only. You should treat their output as read only. In addition, it is possible to define two-way reactive functions with both a setter and a getter. Note that this is a more advanced pattern and should not be used unless you are confident that it is a strong fit for your use case.
// Model functions created with just a function act as getters only.
// These functions update the output path when any input changes
model.fn('expensiveItems', function(items) {
return items && items.filter(function(item) {
return item.price > 100;
});
});
// It is also possible to define both a getter and a setter function
// if the input values may be computed from setting the output
model.fn('fullName', {
// The getter function gets the current value of each of the input
// arguments when any input might have changed
get: function(firstName, lastName) {
return firstName + ' ' + lastName;
},
// The setter function is called with the value that was set at
// the output path as well as the current value of the inputs.
// It should return an array or object where each property is an
// index that corresponds to each input argument that should be set.
// If the function returns null, no items will be set.
set: function(value, firstName, lastName) {
return value && value.split(' ');
}
});
In addition to passing in a function directly, a function can be defined on a model via a name. This name can then be used in place of a function argument.
model.fn(name, fn)
name
A name that uniquely identifies the functionfn
A getter function or an object with the form{get: function(), set: function()}
Reactive functions started on the server via a name are reinitialized when the page loads. In order to add functions for use in routes as well as in the client, use the 'model'
event emitted by apps, which occurs right before an app route is called on the server and once immediately upon initialization in the client. Then, you can safely start them in the appropriate route, and they will be re-established automatically on the client.
In DerbyJS, this pattern is generally less preferable to initializing model functions in a component.
app.on('model', function(model) {
// Sort the players by score and return the top X players. The
// function will automatically update the value of '_page.leaders'
// as players are added and removed, their scores change, and the
// cutoff value changes.
model.fn('topPlayers', function(players, cutoff) {
// Note that the input array is copied with slice before sorting
// it. The function should not modify the values of its inputs.
return players.slice().sort(function (a, b) {
return a.score - b.score;
}).slice(0, cutoff - 1);
});
});
app.get('/leaderboard/:gameId', function(page, model, params, next) {
var game = model.at('game.' + params.gameId);
game.subscribe(function(err) {
if (err) return next(err);
game.setNull('players', [
{name: 'John', score: 4000},
{name: 'Bill', score: 600},
{name: 'Kim', score: 9000},
{name: 'Megan', score: 3000},
{name: 'Sam', score: 2000}
]);
model.set('_page.cutoff', 3);
model.start(
// Output path
'_page.topPlayers',
// Input paths
game.at('players'),
'_page.cutoff',
// Name of the function
'topPlayers'
);
page.render();
});
});