How to Not Build a React App (Part VI)

Trigger effects based on the state not on events!

Back to: Part I

Now we get a chance to demonstrate the “effects based on state” idea. Our state is a discriminated union of:

type State = ConnectedState | DisconnectedState | ConnectingState;

so the (simplified) logic is: “When we are disconnected, we attempt a Websocket connection”. Attempting the connection is the “effect”. The key point is we don’t do this based on any event, we do it based on the state.

We define:

type Effect = (state: State) => State

We are not going down the pure functional programming route of capturing effects in the type system with monads, but it’s implied that all side effect will happen inside those functions. They need to return a new state because we need to track that the effect is in progress, so they don’t rerun.

We connect our effects to the state like so:

subscribe((state) => {
  let newState = state;
  for (const effect of effects) {
    newState = effect(newState);
  }
  if (newState !== state) {
    setState(newState);
  }
});

So every effect sees almost every state, as the state changes. Strictly speaking the order could matter and the position of an effect in the effects array determines what states the effect sees. I don’t anticipate that this will be an issue, but I acknowledge the danger.

Minor points

const tick = createHandler((state) => state);
setInterval(tick, 1000);

This actually doesn’t work very well and we will fix it when when introduce coeffects.


export const removeWsListeners: Effect = (state) => {
  if (state.websocketStatus === "disconnected" && state.ws) {
    state.ws.removeEventListener("close", setDisconnected);
    state.ws.removeEventListener("open", setConnected);
    state.ws.removeEventListener("error", setDisconnected);
    state.remove("ws");
  }
  return state;
};

export const ensureConnection: Effect = (state) => {
  if (
    state.websocketStatus === "disconnected" &&
    state.ws === undefined &&
    (state.failureAt === undefined ||
      state.failureAt.getTime() < Date.now() - RECONNECT_MS)
  ) {
    const ws = new WebSocket(WS_URL);
    ws.addEventListener("close", setDisconnected);
    ws.addEventListener("open", setConnected);
    ws.addEventListener("error", setDisconnected);
    return makeConnectingState({
      ...state.toObject(),
      websocketStatus: "connecting",
      ws,
    });
  }
  return state;
};

Next: Part VII (Wherein it feels like we are starting to reimplement RxJS)