const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
Pass down callbacks that wrap the side effect/attach the callback to the reducer state
export function useDatePicker(props) {
const [[state, effects], dispatch] = useReducer(datePickerReducer, INITIAL);
...
useEffect(() => {
effects.forEach(effect => {
if (effect.type === DATE_SELECTED) {
onDateSelected(effect.selectedDate, effect.selectedTenor);
} else if (effect.type === TENOR_SELECTED) {
onTenorSelected(effect.selectedTenor, effect.selectedDate);
}
});
dispatch({ type: EFFECTS_PROCESSED, effects });
}, [effects, onDateSelected, onTenorSelected]);
return [state, dispatch];
}
https://twitter.com/DavidKPiano/status/1121700680291553281
export function datePickerReducer([state, effects], action) {
switch (action.type) {
case PREVIOUS_MONTH_SELECTED:
return [{ ...state, ...goBackOneMonth(state) }, effects];
case NEXT_MONTH_SELECTED:
return [{ ...state, ...goForwardOneMonth(state) }, effects];
case DATES_DATA_UPDATED:
return datesDataUpdated(state, effects, action);
default:
return [state, effects];
}
}
function datesDataUpdated(state, effects, action) {
const { selectedDate, settlementDates } = action;
const tenorEffect = createTenorEffect(action);
const datePickerInError = settlementDates.includes(selectedDate) === false;
const newState = { ...state, datePickerInError };
const newEffects = tenorEffect ? [...effects, tenorEffect] : effects;
return [newState, newEffects];
}
function dateSelected(state, action, effects) {
const effect = { type: DATE_SELECTED, selectedDate: action.selectedDate };
const newState = dateOrTenorSelected(state);
const tenorsForDate = Object.entries(action.tenors).filter(
([, date]) => date === action.selectedDate
);
// Either no tenor maps to the selected date (broken) or multiple tenors
// do, either which way we only want to send a tenor if there is a single
// result.
if (tenorsForDate.length === 1) {
effect.selectedTenor = tenorsForDate[0][0];
}
return [newState, [...effects, effect]];
}
Statecharts, not state machines.
Creates and runs statecharts.
Statecharts are a formalism for modeling stateful, reactive systems.
In addition to just using statecharts to model the behaviour in documents separate from the actual running code, itβs possible to use one of various machine formats, both to design the behaviour, and at run-time to actually be the behaviour. The idea is to have a single source of truth that describes the behaviour of a component, and that this single source drives both the actual run-time code, but that it can also be used to generate a precise diagram that visualises the statechart.
Rambling thoughts on React and Finite State Machines
https://www.youtube.com/watch?v=WbhpQXH7XMw
export default Machine(
{
id: "blocktrades",
initial: "Initial",
strict: true,
states: {
Initial: {
on: {
Submit: {
actions: ["createFields", "setStatusToRequesting", "subscribe"],
target: "Submitted"
}
}
},
Submitted: {
invoke: [
{
id: "publisher",
src: publishingMachine,
data: context => context
}
],
on: {
ClientClose: "ClientCloseSent",
SubmitAck: { actions: ["submitAck"], target: "Queued" }
}
},
Queued: {
on: {
ClientClose: "ClientCloseSent",
Expire: "Expired",
PickUp: { actions: ["pickUp"], target: "PickedUp" }
}
},
import tradeModelMachine from "./machines/tradeModelMachine";
import BlockTradeMachineContext from "./machines/BlockTradeMachineContext";
export default function BlockTrade({
...
}) {
const [current, send, service] = useMachine(
tradeModelMachine.withContext({
streamLink: require("service!StreamLink"),
store: require("service!caplinps.redux-store")
})
);
return (
<BlockTradeMachineContext.Provider value={{ current, send, service }}>
<BlockTradeFooterContainer
blockId={blockId}
isBlockValid={isBlockValid}
isQuoting={isQuoting}
isLoading={isLoading}
/>
</BlockTradeMachineContext.Provider>
);
export default function BlockTradeFooter(props) {
const { send } = useContext(BlockTradeMachineContext);
return (
<Button
className="execute-button primary"
onClick={() => {
send("Submit", { blockID: blockId });
}}
disabled={!allowQuoteOrExecution}
>
{executionLabel}
</Button>
)
export default Machine(
{
id: "blocktrades",
initial: "Initial",
strict: true,
states: {
Initial: {
on: {
Submit: {
actions: ["createFields", "setStatusToRequesting", "subscribe"],
target: "Submitted"
}
}
},
{
actions: {
createFields: assign(({ store }, { blockID }) => {
const block = store.getState()["blockTrades"].blocks[blockID];
const isTOBO = hasTOBO(store.getState());
const extraFields = getExtraFields(store, blockID, isTOBO);
const fields = mapBlockToSubmitFields(block, extraFields);
const RequestID = `block-${new Date().getTime()}`;
return { blockID, fields, isTOBO, RequestID };
}),
subscribe: assign({
subscriberActorRef: context => spawn(subscriber(context), "subscriber")
}),
function subscriber({ RequestID, streamLink }) {
return function(callback, receive) {
const subscription = streamLink.subscribe(BLOCK_TRADE_CHANNEL, {
onRecordUpdate(subscription, event) {
const fields = event.getFields();
if (RequestID === fields.RequestID) {
callback({ type: fields.MsgType, fields });
}
},
onSubscriptionStatus() {},
onSubscriptionError() {}
});
receive(event => {
if (event.type === "UNSUBSCRIBE") {
subscription.unsubscribe();
}
});
};
}
Submitted: {
invoke: [
{
id: "publisher",
src: publishingMachine,
data: context => context
}
],
on: {
ClientClose: "ClientCloseSent",
SubmitAck: { actions: ["submitAck"], target: "Queued" }
}
},
export default Machine({
id: "publisher",
initial: "GettingTenorDates",
strict: true,
states: {
GettingTenorDates: {
invoke: { id: "tenordatesgetter", src: getTenorDates },
on: { TenorDatesReceived: "Publishing" }
},
Publishing: {
invoke: { id: "publisher", src: publisher },
on: { Published: "Published" }
},
Published: { type: "final" }
}
});
function getTenorDates({ blockID, store: { dispatch, getState }, streamLink }) {
const block = getState()["blockTrades"].blocks[blockID];
const currencyPair = block.selectedCurrencyPair;
const subject = "/CALENDAR/TENORDATES/" + currencyPair;
return function(callback) {
streamLink.subscribe(subject, {
onSubscriptionStatus() {},
onSubscriptionError(subscription, event) {
dispatch(tenorDatesReceived(blockID, { Tenor: "{}" }));
callback("TenorDatesReceived");
},
onRecordUpdate: function(subscription, event) {
dispatch(tenorDatesReceived(blockID, event.getFields()));
subscription.unsubscribe();
callback("TenorDatesReceived");
}
});
};
}
function publisher({ fields, isTOBO, RequestID, streamLink }) {
return function(callback) {
const fieldsToPublish = {
...fields,
...mandatoryFields,
RequestID,
MsgType: "Submit"
};
if (isTOBO) {
fieldsToPublish.TradingSubProtocol = "SALES_RFS";
}
streamLink.publishToSubject(BLOCK_TRADE_CHANNEL, fieldsToPublish, {
onCommandOk() { callback("Published"); },
onCommandError() { }
});
};
}
state β context
action β event
reducer (+ effects code) β actions