...e.g. Form fields, buttons, pure UI
....e.g. HTTP requests, business rules, derived state
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);
});
}
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
Does adding a requirement or step feel like a linear change, or a logarithmic change?
How hard is it to test?
Can you write tests first?
Does changing it feel safe, or scary?
(stolen from CQRS/Event Sourcing)
A long-running state machine that is driven by events
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);
"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
});
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
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)