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.
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.)
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.
onchange
is the default, which is the value of the stream. The stream returned by dropRepeats
only produces values that are different from the previous.onchange
is the value of the counter
property. Notice that the stream
returned by dropRepeats
only produces values when the counter
value is different from the
previous. When the label
value changes but the counter
value is the same, the stream does not
produce a value.By using dropRepeats
, services don't need to worry about avoiding infinite loops.
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:
onchange
: a function that receives the state and returns a value.run
: a function that gets called when the value returned by onchange
changes. The run
function receives the current cell
, from which it can read cell.state
and call
cell.update(...)
to update the state.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.
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.
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:
page
to indicate the current page: "Home"
, "Login"
, "Data"
login
with username
and password
for the Login formdata
to indicate "loading"
or an array of data for the Data page.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.
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.