Two kinds of problems

1. Immediate, atomic changes

...e.g. Form fields, buttons, pure UI

Our Solution

  • Global state is easy to debug
  • Components still treat state as if it's local
  • Components are composable without knowing the global tree's structure

Global state, cursors

2. Sequences of
coordinated changes

....e.g. HTTP requests, business rules, derived state

Our Solution

  • Operates on global state atom (getState)
  • Promises handle sequencing via .then

Events + handlers + promises

Um, what's the problem?

function setPricingContextAsync (structure) {
    let cState = structure.cursor();

    cState = cState.set('isGettingRates', true);

    updateMarketEstimateAsync(structure);
    return executePeriodicAsync(() => updateRatesAsync(structure), { interval: 1000, times: 2 }).then(() => {
        let cState = structure.cursor();

        // If polling returned any rates, turn off the loading indicator
        if (cState.get('rates').count()) {
            cState = cState.set('isGettingRates', false);
            cState = updateActiveView(structure);
        }

        // Continue to poll for rates in the background
        return executePeriodicAsync(() => updateRatesAsync(structure), { interval: 2000, times: 2 });
    }).always(() => {
        let cState = structure.cursor();

        // Always turn off the loading indicator when polling is finished
        cState = cState.set('isGettingRates', false);

        // Check if polling returned any rates, and transition if there are none
        return cState = updateActiveView(structure);
    });
}

Bad and not fun

Inherent complexity

1. (sync) Set 'isUpdating' flag
2. (sync) Update incomplete listing
3. (async) create a new RateRequest
4a. (aysnc) request rates
    a1. poll for rates
        1a. (async) fetch rates
        1b. (sync) update with results
        1c. (async) wait 1 second
        (repeat 4x)
    a2. (sync) maybe set isUpdating flag
    a3. poll for rates
        3a. (async) fetch rates
        3b. (sync) update with results
        3c. (async) wait 2 second
        (repeat 4x)
    a4. (sync) maybe set isUpdating flag
4b. (async) request estimate
    b1. (sync) set 'isRequestingEstimate' flag
    b2. (async) fetch estimate
    b3. (sync) update with estimate

Complexity of expression

Does adding a requirement or step feel like a linear change, or a logarithmic change?

Verifiability

  • How hard is it to test?

  • Can you write tests first?

  • Does changing it feel safe, or scary?

What can we do about it?

SAGA

(stolen from CQRS/Event Sourcing)

A long-running state machine that is driven by events

What does it look like?

import { take, call, fork, update } from '../../components/sagas/io';
import { DATE_RANGE_CHANGED } from '../constants/actions';
import serializeListing from '../effects/serializeListing';
import UpdateCommand from '../commands/UpdateIncompleteListingCommand';

function setDateRange (state, routeStop, range) {
    return state.setIn([routeStop, 'earliestArrival'], range.start)
                .setIn([routeStop, 'latestArrival'], range.end);
}

function* timeframeSaga () {
    while (true) {
        const { dateRange, datepickerId, state } = yield take(DATE_RANGE_CHANGED);     
        const range = dateRange;
        const routeStop = getRouteStop(datepickerId);

        if (!rangeWasUpdated(state, routeStop, range)) continue;

        const updatedState = yield update(setDateRange, routeStop, range);
        const serializedModel = yield call(serializeListing, updatedState);
        yield fork(UpdateCommand.runAsync, serializedModel);
    }
}

Saga.withAtom(structure)
    .run(timeframeSaga);

Two key concepts

1. Declarative effects

"Things that happen" are just plain JS objects, which means they're super testable

// describes pausing until FOO_ACTION occurs
const takeEffect = take(FOO_ACTION);

expect(takeEffect).to.deep.equal({
   TAKE: true,
   pattern: FOO_ACTION
});

// describes calling the function fetchSomething with argument 'id'
const callEffect = call(fetchSomething, id);

expect(takeEffect).to.deep.equal({
   CALL: true,
   fn: fetchSomething,
   args: id
});

2. Inversion of control

function* persistenceSaga (model) {
    
    // Yields control with a 'call' effect description
    // Process runner runs the call, and returns control with the result
    const serializedModel = yield call(serialize, model);

    try {
        yield call(updateModelAsync, serializedModel);
    } catch (e) {
        // even logging is a side effect
        yield call(log, `Update failed with error ${e.message}`);
    }

}

Isolates the actual running of side-effects in one place, which makes declarative effects possible

1. (sync) Set 'isUpdating' flag
2. (sync) Update incomplete listing
3. (async) create a new RateRequest
4a. (aysnc) request rates
    a1. poll for rates
        1a. (async) fetch rates
        1b. (sync) update with results
        1c. (async) wait 1 second
        (repeat 4x)
    a2. (sync) maybe set isUpdating flag
    a3. poll for rates
        3a. (async) fetch rates
        3b. (sync) update with results
        3c. (async) wait 2 second
        (repeat 4x)
    a4. (sync) maybe set isUpdating flag
4b. (async) request estimate
    b1. (sync) set 'isRequestingEstimate' flag
    b2. (async) fetch estimate
    b3. (sync) update with estimate

Remember this?

yield update(setIsUpdating, true)
yield update(applyShipmentEdits, edits)
yield call(createRateRequest, state)
yield fork(requestRates* () {
    yield callRepeated(4, pollForRates* () {
        yield call(fetchRates, rateRequestId)
        yield update(setRates, rates)
        yield wait(1000)
    if (rates) yield update(setIsUpdating, false)
    yield callRepeated(pollForRates* () {
        yield call(fetchRates, rateRequestId)
        yield update(setRates, rates)
        yield wait(2000)
    yield update(setIsUpdating, false)
yield fork(requestEstimate* () {
    yield update(isRequestingEstimate, true)
    yield call(fetchEstimate, state)
    yield update(setEstimate, estimate)

Maps almost 1:1

Concerns

  • ANOTHER concept???
  • Generators are hard to grok
  • Uncanny valley resemblance to CSP
  • What else?

Sagas

By Daniel Poindexter

Sagas

  • 561