Meiosis Tutorial

< Previous | Next > | Table of Contents

05 - Meiosis with Mergerino

In the previous lesson, 04 - Meiosis with Function Patches, we set up the Meiosis pattern with an update stream of function patches.

In this section, we will use another approach - my personal favourite - using a library called Mergerino. The Meiosis pattern is flexible enough that you can use either of these approaches or even one of your own.

Introducing Mergerino

Mergerino is a brilliant utility that Daniel Loomer wrote in less than 30 lines of code. We will use it to issue patches onto our update stream, and to produce the updated state from our accumulator function.

Let's say we have this initial state:

{
  temperature: {
    value: 22,
    units: "C"
  }
}

Imagine that our patches are objects that describe how we want to update the state. If we want to change the temperature value to 23, we would call:

update({ value: 23 })

To change the units:

update({ units: "F" })

To convert the value at the same time as changing the units:

update({ value: 72, units: "F" })

How to we write an accumulator function that handles these object patches to update the state?

Mergerino comes with a function, merge, that takes a target object as its first parameter, and patch objects in the remainder of the parameters. It patches the target object by copying over the properties from the patch objects onto the target object:

merge({ value: 22, units: "C" }, { value: 23 })
// result:
{ value: 23, units: "C" }

merge({ value: 23, units: "C" }, { comfortable: true })
// result:
{ value: 23, units: "C", comfortable: true }

If you find that this looks like Object.assign, you are correct: merge does the equivalent. However, merge has more capabilities.

Patching based on the current value

Within a patch, you can use the current value of the target object to determine the updated value. Just pass a function as the value of the property. Mergerino passes the value of that property to the function, and assigns the function's return value back to that property.

This makes it easy for us to update a value using the previous value. For example, say that we want to increment the temperature value by 1. We need the previous value to compute the updated value. We can use a function for value:

merge({ value: 22, units: "C" }, { value: x => x + 1 }) // The function receives 22
// result:
{ value: 23, units: "C" }

Note that x => x + 1 is ES6 syntax that is short for

function(x) {
  return x + 1;
}

By passing a function for the value property, Mergerino passes the previous value of that property to the function. Our function receives 22, adds 1 and returns 23, which Mergerino assigns back to the value property.

Deep Patching

Object.assign performs a shallow merge. If our target object is:

{ air:   { value: 22, units: "C" },
  water: { value: 84, units: "F" }
}

And we want to change the air value to 25 by calling:

Object.assign(
  { air:   { value: 22, units: "C" },
    water: { value: 84, units: "F" }
  },
  { air:   { value: 25 } }
)

We get this result:

{ air:   { value: 25 },
  water: { value: 84, units: "F" }
}

We lost the units! This is because properties are merged only at the first level.

With Mergerino, we can merge properties deeper than the first level without losing the rest:

merge(
  { air:   { value: 22, units: "C" },
    water: { value: 84, units: "F" }
  },
  { air: { value: 25 } }
)
// result:
{ air:   { value: 25, units: "C" }, // now we didn't lose the units!
  water: { value: 84, units: "F" }
}

Deep patching and function patching can also be used together:

merge(
  { air:   { value: 22, units: "C" },
    water: { value: 84, units: "F" }
  },
  { air:  { value: x => x + 8 } }
)
// result:
{ air:   { value: 30, units: "C" }, // we increased the value by 8, and didn't lose the units
  water: { value: 84, units: "F" }
}

If we want to avoid deep patching and instead want to replace a property, we can use a function. Say we want to set air to { replaced: true } without keeping value and units:

merge(
  { air:   { value: 22, units: "C" },
    water: { value: 84, units: "F" }
  },
  { air:   () => ({ replaced: true }) } // use a function to replace the value
)

We get this result:

{ air:   { replaced: true },
  water: { value: 84, units: "F" }
}

Deleting a property

Finally, we can use undefined as a property value when we wish to delete that property:

merge(
  { air:   { value: 22, units: "C" },
    water: { value: 84, units: "F" }
  },
  { air: undefined }
)
// result:
{ water: { value: 84, units: "F" } }

merge(
  { air:   { value: 22, units: "C" },
    water: { value: 84, units: "F" }
  },
  { air: { value: undefined } }
)
// result:
{ air:   { units: "C" },
  water: { value: 84, units: "F" }
}

Try it out. Using the code window below, try the following exercises. Use console.log to verify your answers.

Exercises

  1. Change water to { value: 84, units: "F" }
  2. Toggle the comfortable property with a function that changes the value to the opposite of what it was
  3. Change the air value to 20 without losing the units
  4. Delete the invalid property.

Solution

Show solution

Using Mergerino with Meiosis

To use Mergerino with Meiosis, we can pass object patches onto the update stream and use them in the accumulator to update the state.

For example, to increment the temperature value:

increment: function(amount) {
  update({
    temperature: {
      value: x => x + amount
    }
  });
}

Now we need to use these object patches in the accumulator function. Remember that the accumulator gets the current state and the incoming patch as parameters, and must return the updated state. We can use merge:

var states = flyd.scan(function(state, patch) {
  return merge(state, patch);
}, temperature.Initial(), update);

Notice that the accumulator function that we are passing is:

function(state, patch) {
  return merge(state, patch);
}

We have a function that takes (state, patch) and calls merge with (state, patch). But merge already does what we want, so we can pass it directly:

var states = flyd.scan(merge, temperature.Initial(), update);

Putting it all together, we have:

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.increment(2)

actions.changeUnits()

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

When you are ready, continue on to 06 - Components.

< Previous | Next > | Table of Contents


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