The Boilerplate

Application Anatomy

Every ferp application uses the app function, and must have init, which declares your applications initial state, and any initial side effect. In the helloWorld example, there is no state, null, and a custom side effect that adds text to the html page or console, writeEffect.

init: [state, effect]

import { app, effects } from 'ferp';

const INITIAL_STATE = { counter: 1 };

app({ init: [INITIAL_STATE, effects.none()] });

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.

subscribe: (state) => []

import { app, effects } from 'ferp';

import mySubscription from './mySubscription.js';

const INITIAL_STATE = { counter: 1 };

app({
  init: [INITIAL_STATE, effects.none()],
  
  subscribe: (state) => [mySubscription, state.counter < 10]
});

This method lets you run subscriptions, which are long-running effects that react to state by starting, restarting, or stopping. Subscriptions are great ways to interact with third-party objects, like websockets, dom watching mechanisms, or other event-based long-running tasks. You can read more about them in the What is a Ferp Subscription? section.

observe: ([currentState, currentEffect], actionAnnotation) => {}

import { app, effects } from 'ferp';

const INITIAL_STATE = { counter: 1 };

const incrementCounter = (state) => [
  { counter: state.counter + 1 },
  effects.none(),
];

app({
  init: [INITIAL_STATE, effects.act(incrementCounter)],
  
  observe: ([state, effect], annotation) => {
    console.groupCollapsed(annotation, state);
    console.log(effect);
    console.groupEnd();
  }
});

Observe is an optional app property that lets you see the current state and effect being ran, as well as the action that triggered it (if available). This can be a great way to react to every state change, and not just the changes a subscription would see. For example, if you write a canvas application that must draw after every state update, putting the draw logic in observe may be what you're looking for. Another example would be for an application debug tool, where you output the current state, action, and effects being ran.

const dispatch = app({ ... })

import { app, effects } from 'ferp';

const INITIAL_STATE = { counter: 1 };

// Action
const incrementCounter = (state) => [
  { counter: state.counter + 1 },
  effects.none(),
];

// Action-creator
const incrementCounterBy = (amount) => (state) => [
  { counter: state.counter + amount },
  state.none();
];

const dispatch = app({
  init: [INITIAL_STATE, effects.none()],
});

dispatch(incrementCounter); // Dispatch an action
dispatch(incrementBy(5), 'incrementBy'); // Dispatch an action creator, with an annotation

Dispatch is how you run new actions to update your state and run additional side effects, and allows subscriptions to react to your state changes. You can call dispatch from anywhere with any action-function. Dispatch can take an action or an action creator, and an optional action annotation. Annotations can be handle for action creators, which don't have function names by default.

The Counter App

import { app, effects } from 'https://unpkg.com/ferp?module=1';

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 elementSetTextEffect = (elementId, text) => effects.thunk(() => {
  const element = document.getElementById(elementId);
  if (element) {
    element.innerText = text.toString();
  }
  
  return effects.none();
});
const elementSetText = (elementId) => function elementSetTextAction(state) { return [
  state,
  elementSetTextEffect(elementId, state.toString()),
]; };

const Incr = (state) => [state + 1, effects.act(elementSetText('value'))];
const Decr = (state) => [state - 1, effects.act(elementSetText('value'))];

const elementClickSubscription = (dispatch, elementId, clickAction) => {
  const callback = (event) => {
    dispatch(clickAction, `click:${elementId}`);
  };
  
  const element = document.getElementById(elementId);
  element.addEventListener('click', callback);
  
  return () => {
    element.removeEventListener('click', callback);
  };
};

const initialState = 0;

app({
  init: [
    initialState,
    effects.act(elementSetText('value'))
  ],
  
  subscribe: () => [
    [elementClickSubscription, 'incr', Incr],
    [elementClickSubscription, 'decr', Decr],
  ],
});

Last updated