Meiosis Tutorial

< Previous | Next > | Table of Contents

06 - Components

Building Components

Let's continue our previous example. We already had a "temperature" component. We'll add a "conditions" component for the current conditions (sunny, cloudy, rain) so that our initial state is now:

{
  conditions: {
    precipitations: false,
    sky: "Sunny"
  },
  temperature: {
    value: 22,
    units: "C"
  }
}

Each component will separately indicate its initial state and the actions that it provides. At the top-level app, we'll assemble these together into the complete initial state and set of actions.

For the conditions, we have:

var conditions = {
  Initial: function() {
    return {
      conditions: {
        precipitations: false,
        sky: "Sunny"
      }
    };
  },
  Actions: function(update) {
    return {
      togglePrecipitations: function(value) {
        update({ conditions: O({ precipitations: value }) });
      },
      changeSky: function(value) {
        update({ conditions: O({ sky: value }) });
      }
    };
  }
};

For the temperature, we have essentially the same code as we previously had.

var temperature = {
  Initial: function() {
    return {
      temperature: {
        value: 22,
        units: "C"
      }
    };
  },
  Actions: function(update) {
    return {
      increment: function(amount) {
        update({ temperature: O({ value: O(x => x + amount) }) });
      },
      changeUnits: function() {
        update({
          temperature: O(state => {
            var value = state.value;
            var newUnits = state.units === "C" ? "F" : "C";
            var newValue = convert(value, newUnits);
            state.value = newValue;
            state.units = newUnits;
            return state;
          })
        });
      }
    };
  }
};

Then, to assemble the components into the top-level app, we put together the state management code by combining the initial state and the actions of the components.

var app = {
  Initial: function() {
    return O({},
      conditions.Initial(),
      temperature.Initial()
    );
  ),
  Actions: function(update) {
    return O({},
      conditions.Actions(update),
      temperature.Actions(update)
    );
  }
};

Here is the complete example:

In this example, components designate a property for their state (conditions, temperature). What if we want to designate a property from outside the component, to make sure there are no conflicts? Even more significantly, what if we want to have multiple instances of a component?

Using IDs for Components

Whether it's to manage properties from outside of components, or to use multiple instances of a component, we can use IDs and pass them to components. Then, instead of having a hardcoded property in the component, the ID is used when reading and updating state.

Continuing the previous example, let's say we want to have two instances of the temperature component: one for the air temperature and one for the water temperature. We want to use the air and water properties in the application state.

We'll change the actions to accept an id parameter. Then, we use the id when issuing updates, so that we dyamically update the id property of the state:

Actions: function(update) {
  return {
    increment: function(id, amount) {
      update({ [id]: O({ value: O(x => x + amount) }) });
    },
    changeUnits: function(id) {
      update({
        [id]: O(state => {
          var value = state.value;
          var newUnits = state.units === "C" ? "F" : "C";
          var newValue = convert(value, newUnits);
          state.value = newValue;
          state.units = newUnits;
          return state;
        })
      });
    }
  };
}

Notice the { [id]: ... } syntax which creates an object with a dynamic id property.

Now, we create the initial state with two instances of temperature, one with air and one with water. The actions are created the same as before. Indeed, we just need one instance of the temperature actions; it's the id that we pass to the actions that indicates which instance to act upon.

var app = {
  Initial: O({},
    conditions.Initial(),
    { air: temperature.Initial() },
    { water: temperature.Initial() }
  ),
  Actions: function(update) {
    return O({},
      conditions.Actions(update),
      temperature.Actions(update)
    );
  }
};

Here is the complete example:

Exercises

Try it out: notice that the initial state appears in the output on the right. Within the console, type and then press Enter:

actions.changeSky("Cloudy")

actions.increment("air", 2)

actions.changeUnits("water")

In the output on the right, you'll see the updated states.

State Management and View code

So far, all we have is state management code; there is no view code. This is on purpose: we've built our state management code independently. We can trigger actions and see the updated state. Our code is not tied to any particular view code.

Having independent state management code makes it easier to test and debug. You can organize and assemble actions as you prefer. For example, if you find that the number of actions in your application is getting large, you might decide to namespace your actions by grouping them under properties, such as actions.conditions.changeSky and actions.temperature.increment.

The view code is a separate concern. It will display the UI based on the state, and call actions. No matter which view library you use, it is easy to wire up because views only depend on state and actions.

How you group together the code is up to you. In some cases, it might make sense to put state management and view code together into a folder for a component of your application. In other cases, you may prefer to have view components separate from the code that manages state.

In the following sections, we will look at wiring up the Meiosis pattern to a handful of view libraries. Feel free to jump straight to the view library in which you are interested.

< Previous | Next > | Table of Contents


Meiosis is developed by @foxdonut00 / foxdonut and is released under the MIT license.