What is a Ferp Action?

Actions

Actions are the only way to initiate new state changes and side-effects in Ferp. State changes must be complete and immutable, and side effects must always be declared, even if there are no side-effects. Actions are always pure functions that return [state, sideEffect], and pure meaning it does not use any data not immediately accessible to the action. On the contrary, an invalid action would use things like Math.random() or Date.now(), which are external to the action function.

Types of Actions

Naked Action

Plain, or naked, actions are actions that need no parameters. For instance, a naked action could be something that only updates a single state property by incrementing it by 1. Let's see what that looks like:

import { effects } from 'ferp';

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

Depending on the shape of your state, you may find naked actions to be very fitting.

There are two ways to call an action, and here they are:

import { app, effects } from 'ferp';

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

const dispatch = app({
  init: [
    { counter: 0 },
    effects.act(IncrementCounterByOne), // run an action via effect
  ],
});

dispatch(IncrementCounterByOne); // run an action via dispatch

Action Builder

Action builders are actions that can take parameters. This may mean a value can be passed from user input, or from some other external side-effect. Using the previous counter example, here's what it would look like if it took external input for the increment amount:

import { effects } from 'ferp';

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

Similar to naked actions, there are two ways to run this action in your application:

import { app, effects } from 'ferp';

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

const dispatch = app({
  init: [
    { counter: 0 },
    effects.act(IncrementCounterByN(10)), // run a builder action via effect
  ],
});

dispatch(IncrementCounterByN(999)); // run a builder action via dispatch

If you're not sure how the builder works, this is just a function that returns a function. Here's probably a more familiar version to read:

import { effects } from 'ferp';

function IncrementCounterByNBuilder(n) {
  return function IncrementCounterByNAction(state) {
    return [
      { ...state, counter: state.counter + n },
      effects.none(),
    ];
  };
};

This is called a closure, and it's used in Javascript to encapsulate a scope. For instance, the variable n is available in the inner function (IncrementCounterByNAction), but n is not available outside of this closure (IncrementCounterByNBuilder).

To make this easier to understand, once we have this builder that returns an action, internally, Ferp does something like this:

// You call...
dispatch(IncrementCounterByNBuilder, 999);

// Ferp does...
const yourAction = IncrementByNBuilder(999);
dispatch(yourAction);

Last updated