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