Core Concepts

ItsyUI follows the same concepts as Redux (but without the application level state). Each component/widget maintains its own state, and each widget publishes transitions that can be received by other widgets. It is an abstracted definition of Pub/Sub model for widgets.

As an example, your application state needs to maintain a counter value, the state will look like this,

const initialState = {
  counter: 0
};

This object is like a “model” except that there are no setters. This is so that different parts of the code can’t change the state arbitrarily, causing hard-to-reproduce bugs.

To change something in the state, you need to dispatch an action. An action is a plain JavaScript object (notice how we don’t introduce any magic?) that describes what happened. Here are a few example actions:

{ type: "UP_COUNT" }
{ type: "DOWN_COUNT" }

Finally, to tie state and actions together, we write a function called a reducer. Again, nothing magical about it—it’s just a function that takes state and action as arguments, and returns the next state of the app. It would be hard to write such a function for a big app, so we write smaller functions managing parts of the state:

function reducer(state, action) {
  switch (action.type) {
    case "ADD":
      return {
        ...state,
        count: state.count + 1
      };
    case "DEC":
      return {
        ...state,
        count: state.count - 1
      };
    case "RESET":
      return {
        count: 0
      };
    default:
      return state === undefined ? initialState :
        Object.keys(state).length === 0 ? initialState : state;
  }
}

This follows Redux way of state management. One key difference with ItsyUI is the state is managed at the widget and not as an application state, this means, there could be a container widget called <App/>, and the state within this widget can be for the whole application.

Additionally, ItsyUI introduces FSM (Finite State Machine) within each control to define the flow of the control. A typical state FSM looks like this,

const stateJSON = {
  "initial": "onLoaded",
  "states": {
    "onLoaded": {
      "on": {
        "BEFORE_UP_COUNT": "beforeCounterInc",
        "DOWN_COUNT": "counterDec",
      }
    },
    "beforeCounterInc": {
      "onEntry": [
        "onBeforeCounterInc"
      ],
      "on": {
        "UP_COUNT": "counterInc"
      }
    },
    "counterInc": {
      "onEntry": [
        "onCounterInc"
      ],
      "on": {
        "ON_LOADED": "onLoaded"
      }
    },
    "counterDec": {
      "onEntry": [
        "onCounterDec"
      ],
      "on": {
        "ON_LOADED": "onLoaded"
      }
    },
  }
};

Note: ItsyUI uses the awesome xstate library for FSM. It abstracts the xstate state machines and blends the state machinery of redux.

For each stateJSON onEntry transition, It needs a corresponding action method to handle the transition. This is defined in a mapDispatchToProps function object,

const mapDispatchToProps = (dispatch) => {
  return {
    onBeforeCounterInc: (evt) => dispatch(doBeforeCounterInc(evt)),
    onCounterInc: (evt) => dispatch(doCounterInc(evt)),
    onCounterDec: (evt) => dispatch(doCounterDec(evt))
  }
};

Each corresponding action has an implementation where the logic is processed,

export function doBeforeCounterInc(evt) {
  return async (getState, dispatch, transition) => {
    transition({
      type: "UP_COUNT"
    });
  };
}

export function doCounterInc(evt) {
  return async (getState, dispatch, transition) => {
    // update the internal state
    dispatch({
      type: "ADD"
    });
    // complete the transition by going to ON_LOADED state
    transition({
      type: "ON_LOADED"
    })
  };
}

export function doCounterDec(evt) {
  return async (_, dispatch, transition) => {
    dispatch({
      type: "DEC"
    });
    transition({
      type: "ON_LOADED"
    })
  };
}

Define your widget with useTransition hook, the useTransition hook accepts a reducer, dispatcher and stateJSON.

import { useTransition } from "@itsy-ui/core";

export function Counter() {
  const [state, transition] = useTransition("Counter", reducer, mapDispatchToProps, stateJSON);
  return (
    <div>
      <p>You clicked {state.count} times</p>
      <button onClick={() => transition({ type: "BEFORE_UP_COUNT" })}>
        ADD
            </button>
      <button onClick={() => transition({ type: "DOWN_COUNT" })}>
        DEC
            </button>
    </div>
  );
}

State machines allows the widgets to do the following,

  1. Trigger state change from an external widget
  2. Allow overriding of state thru JavaScript functions, and change the state of the widget

Overriding state changes or behaviors is covered in next set of documentation.