Meiosis Documentation

< Previous | Next > | Table of Contents

08 - Services

All credit goes to James Forbes for his idea of services. I am very grateful to James for sharing this and other ideas that have significantly contributed to my Meiosis journey.

James explains that while one-off actions occur after click events, user input, and so on, services are for ongoing state synchronization. They can produce computed properties, store and retrieve state from local storage, fetch data from a server, trigger other actions, and so on.

In this section, we will look at how services work in Meiosis.

Services Overview

In Meiosis, services are functions that run every time the state changes. Services can alter the state before the final state arrives onto the states and cells streams. To change the state, services call cell.update(...), or call actions, in the same manner as views. After all services have executed, the resulting state is rendered by the view.

Services can call cell.update(...) both synchronously and/or asynchronously (such as to load data from a server.)

Avoiding Infinite Loops

Because a service runs every time the state changes, and a service changes the state, we run the risk of having an infinite loop. To avoid this, we'll use a function called dropRepeats (again, credit to James) which takes a stream and an optional onchange function. The result is a new stream that only produces a value if it is different from the previous value. By default, the value is the stream's value. If the onchange function is provided by the caller, then the value returned by that function is used to compare to the previous value.

Here is the dropRepeats function. Below it, you will see it in action.

By using dropRepeats, services don't need to worry about avoiding infinite loops.

Services

A service function receives the current cell and can call cell.update(...) to alter the state.

We can use service functions for computed properties, setting up an initial blank state for a page, cleaning up state after leaving a page, and any other state changes that we want to perform synchronously before rendering the view.

Services can also call cell.update(...) asynchronously, such as loading data or triggering other types of asynchronous updates.

Finally, services may perform side effects without changing the state, such as saving state to local storage.

To define a service, we'll create an object with two properties:

const service = {
  onchange: (state) => state.someProperty,
  run: (cell) => {
    // ...
    cell.update(...);
  }
};

The onchange function is optional. If not provided, the run function gets called on every state change.

Pattern Setup

Let's see how we can set up services with Meiosis. We'll start with the base pattern:

const update = m.stream();
const states = m.stream.scan(merge, app.initial, update);
const createCell = (state) => ({ state, update });

Next, given a services array of services, for each service we'll use dropRepeats, passing the service's onchange, and map the resulting stream to call the service's run function:

services.forEach((service) => {
  dropRepeats(states, service.onchange).map((state) =>
    service.run(createCell(state))
  );
});

This will call each service and update the state as each service calls cell.update, with dropRepeats avoiding infinite loops.

Finally, we'll create our cells stream also using dropRepeats:

const cells = dropRepeats(states).map(createCell);

You can see the complete pattern setup below.

Notice that for Mithril we're using m.redraw to make sure the view is re-rendered because we're updating the state with services, thus outside of Mithril's auto-redraw scope.

Services - Example

Let's look at an example using services.

Say we have an app with three pages: Home, Login, and Data. We'll use services to achieve the following:

We'll use these properties in the state:

The login service checks whether the current page is "Login". If so, and the login form has not yet been set up, it updates the state to set up the form with a blank username and password.

If the current page is not "Login", the service removes the login form from the state.

const loginService = {
  // call the service when the page changes
  onchange: (state) => state.page,
  run: (cell) => {
    if (cell.state.page === "Login") {
      cell.update({
        login: { username: "", password: "" }
      });
    } else {
      cell.update({ login: undefined });
    }
  }
};

The data service checks whether the page has changed to "Data". If so, the service sets the state data to "loading". The view uses this to display a Loading, please wait... message. The service calls actions.loadData to simulate loading data asynchronously from a server.

If the page has changed to something other than "Data", the service clears the data property from the state.

const actions = {
  loadData: (cell) =>
    setTimeout(
      () =>
        cell.update({
          data: ['One', 'Two']
        }),
      1500
    )
};

const dataService = {
  onchange: (state) => state.page,
  run: (cell) => {
    if (cell.state.page === "Data") {
      cell.update({ data: "loading" });
      actions.loadData(cell);
    } else {
      cell.update({ data: undefined });
    }
  }
};

Our app contains the initial state, the array of services, and the view:

const app = {
  initial: {
    page: "Home"
  },

  services: [loginService, dataService],

  view: (cell) => ...
};

You can see the complete example in action below.

Conclusion

In this section, we've augmented our Meiosis pattern setup with services. We do not need a lot of code for this setup; nevertheless, for your convenience, you can also use the same setup by adding meiosis-setup to your project.

In the next section, we'll look at another feature that we can add to Meiosis: nesting.


< Previous | Next > | Table of Contents


Meiosis is developed by foxdonut (Twitter / GitHub) and is released under the MIT license.