Building PTA
View
What about...
- state/data management?
- domain logic (code structure/placement)?
- side effects?
Only React components
Redux
Hooks
XState
Redux
- No opinion on side effects.
- Single global store for state.
- Possible scaling/performance issues.
- Global namespace, possible clashes.
Hooks
- No support for side effects.
- Possible scalability concerns (An alternative to useState)?
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>
</>
);
}
Side effects
-
Pass down callbacks that wrap the side effect/attach the callback to the reducer state
- Put the effect code in the React component
- Execute the callback in the reducer
- Return effect objects that describe what the code should do next.
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]];
}
XState
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.
XState
- New concepts, new APIs
- Not a lot of examples where it controls state/code with React
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() { }
});
};
}
Redux, useReducer, XState
state ≈ context
action ≈ event
reducer (+ effects code) ≈ actions
?
PTA
By briandipalma
PTA
- 341