The Boilerplate

Hello World!

helloWorld.browser.js
helloWorld.cli.js
helloWorld.browser.js
import { app, effects } from 'ferp';
const appendTextEffect = (text) => {
document.body.appendChild(document.createTextNode(text));
return effects.none();
};
app({
init: [null, appendTextEffect('hello world!')],
update: (_, state) => [state, effects.none()],
});
helloWorld.cli.js
import { app, effects } from 'ferp';
const consoleEffect = (...args) => {
console.log(...args);
return effects.none();
};
app({
init: [null, consoleEffect('hello world!')],
update: (_, state) => [state, effects.none()],
});

Anatomy 101

Every ferp application uses the app function, and must have two keys, init, and update.

init: [state, effect]

The init key holds a special structure that is used everywhere in ferp. The state-effect tuple tells ferp what the next state should look like, and if there are any effects that need to be ran. The state can take on whatever shape you need it to, but it is pretty common to have large nested objects. This, however, will depend on what you need your application to do, and the data sources it uses. The second part of this tuple is the effect. You can read more about effects in the Core Effects section. In this example, there's an effect that adds text to an HTML document, but more importantly, returns effects.none(), which tells ferp that we're done doing work until some other event triggers our application later.

update: (action, state) => [nextState, effect]

The update function takes in message effects from either init or external sources, and attempts to update the application state. It also takes the current state of the app, so you can spread and update that state as you see fit. That said, no matter how you handle messages and update your state, you must return a tuple of state and effect. Unlike some frameworks, the effect is not optional, you must include it, but you can always use the none effect if you don't want any other work to be done.

The Counter App

counter.browser.js
counter.cli.es
counter.browser.js
import { app, effects } from 'ferp';
const createElement = (tag, id, text) => {
const element = document.createElement(tag);
element.id = id;
element.innerText = text;
return element;
};
document.body.appendChild(createElement('h1', 'value', '0'));
document.body.appendChild(createElement('button', 'incr', '+1'));
document.body.appendChild(createElement('button', 'decr', '-1'));
const messages = {
incr: 'incr',
decr: 'decr',
};
const elementClickSubscription = (elementId, nextEffect) => (dispatch) => {
const callback = (event) => {
dispatch(nextEffect);
};
const element = document.getElementById(elementId);
element.addEventListener('click', callback);
return () => {
element.removeEventListener('click', callback);
};
};
const elementSetText = (elementId, text) => {
const element = document.getElementById(elementId);
if (element) {
element.innerText = text.toString();
}
return effects.none();
};
const incrementEffect = effects.thunk(() => messages.incr);
const decrementEffect = effects.thunk(() => messages.decr);
const updateValue = (nextState) => [
nextState,
elementSetText('value', nextState)
];
const initialState = 0;
app({
init: [initialState, elementSetText('value', initialState)],
update: (message, previousState) => {
switch (message) {
case messages.incr:
return updateValue(previousState + 1);
case messages.decr:
return updateValue(previousState - 1);
default:
return [previousState, effects.none()];
}
},
subscribe: () => [
[elementClickSubscription, 'incr', incrementEffect],
[elementClickSubscription, 'decr', decrementEffect],
],
});
counter.cli.es
#!/usr/bin/env node
/*
Usage:
./counter.cli.es up every 1000ms 5times
The main parts being "up", "1000ms", "5times"
"up" can be replaced with "down"
"1000ms" to any millisecond amount, delay between updates
"5times" to any {number}times, the number of times to continue the loop.
*/
import { app, effects } from 'ferp';
const messages = {
tick: 'tick',
};
const directions = {
up: 1,
down: -1,
};
const delayEffect = (milliseconds, effectRunner) => effects.thunk(() => (
effects.defer(new Promise((resolve) => {
setTimeout(() => resolve(effectRunner()), milliseconds);
}))
));
const consoleEffect = (...args) => {
console.log(...args);
return effects.none();
};
const tickEffect = () => messages.tick;
const getDirection = (fallback) => process.argv.concat(fallback).find(arg => typeof directions[arg] !== 'undefined');
const getTimes = (fallback) => process.argv.concat(fallback).find(arg => /\d+times$/.test(arg));
const getDelay = (fallback) => process.argv.concat(fallback).find(arg => /\d+ms$/.test(arg));
const timesParamToNumber = (timesWithX) => parseInt(timesWithX.replace(/\D/g, ''), 10);
const delayParamToNumber = (delayWithMs) => parseInt(delayWithMs.replace(/\D/g, ''), 10);
const initialState = {
value: 0,
direction: getDirection('up'),
times: timesParamToNumber(getTimes('10times')),
delay: delayParamToNumber(getDelay('1000ms')),
};
app({
init: [initialState, tickEffect()],
update: (message, previousState) => {
if (message !== messages.tick) return [previousState, effects.none()];
const times = previousState.times - 1;
const value = value + directions[previousState.direction];
const effect = times === 0
? effects.none()
: delayEffect(previousState.delay, tickEffect);
return [
{
...previousState,
value,
times,
},
effects.batch([
consoleEffect('Counter: ', value),
effect,
])
];
}
});

Anatomy 102

The last relevant part of the ferp app anatomy is subscribe. Which is used to create long-term effects that can be cancelled.

subscribe: state => []

The subscribe function takes in the current state, and outputs an array of subscription descriptors. A subscription descriptor takes the form of an array, where the first element is the subscription function, and the following values in the array are arguments. You can read more about subscriptions in the What is a Ferp Subscription section.