Giter Site home page Giter Site logo

mergerino's Introduction

Mergerino npm size

An immutable merge util for state management.

Mergerino works very well with the meiosis state management pattern and is offered as a setup option.

Click here to see available builds of mergerino on npm.

ESM installation

import merge from 'https://unpkg.com/mergerino?module'

const state = {
  user: {
    name: 'John',
    weight: 180,
    age: 34,
    height: 177
  },
  other: {
    many: true,
    properties: true
  }
}
const newState = merge(state, {
  user: {
    name: 'Bob',
    weight: undefined,
    age: age => age / 2
  },
  other: () => ({ replaced: true })
})

/*
result = {
  user: {
    name: 'Bob',
    age: 17,
    height: 177
  },
  other: {
    replaced: true
  }
}
*/

playground

  • state is left intact
  • each part of state that your patch instruction touched will be shallow copied into newState
  • untouched properties retain the same references.

ES5 browser installation

<script src="https://unpkg.com/mergerino"></script>
<script>
  const state = {
    user: {
      name: 'John',
      weight: 180,
      age: 34,
      height: 177
    },
    other: {
      many: true,
      properties: true
    }
  }
  const newState = mergerino(state, {
    user: {
      name: 'Bob',
      weight: undefined,
      age: function (age) {
        return age / 2
      }
    },
    other: function () {
      return { replaced: true }
    }
  })

  /*
  result = {
    user: {
      name: 'Bob',
      age: 17,
      height: 177
    },
    other: {
      replaced: true
    }
  }
  */
</script>

playground

Usage Guide

Mergerino is made up of a single function merge(target, ...patches).

Patches in mergerino are expressed as plain JavaScript objects:

merge(state, { I: { am: { just: { an: 'object' } } } })

Mergerino merges immutably meaning that the target object will never be mutated (changed). Instead each object along the path your patch specifies will be shallow copied into a new object.

The advantage of this is that patch operations are relatively quick because they only copy the parts of the object that are touched.

This means you can use strict equality (===) to determine where an object was changed by a patch operation:

const state = { obj: {} }
const newState = merge(state, { newProp: true })
console.log(state === newState) // false
console.log(state.obj === newState.obj) // true

If you want to fully remove a property from an object specify undefined as the value.

const state = { deleteMe: true }
const newState = merge(state, { deleteMe: undefined })
console.log(state) // { deleteMe: true }
console.log(newState) // {}

Use null instead of undefined if you don't want the key to be deleted.

If you want to replace a property based on its current value, use a function.

const state = { age: 10, obj: { foo: 'bar' } }
const newState = merge(state, { age: x => x * 2, obj: () => ({ replaced: true }) })
console.log(state) // { age: 10, obj: { foo: 'bar' } }
console.log(newState) // { age: 20, obj: { replaced: true } }

If you pass a function it will receive the current value as the first argument and the merge function as the second. The return value will be the replacement. The value you return will bypass merging logic and simply overwrite the property. This is useful when you want to replace an object without merging. If you would like to merge from within a function patch then use the merge function provided as the second argument.

Multiple Patches

You can pass multiple patches in a single merge call, array arguments will be flattened before processing.

All the following are valid:

merge(state, [{}, {}, {}])
merge(state, {}, {}, {})
merge(state, [[[[{}]]]])
merge(state, [{}], [{}], [{}])

Another nice side effect of flattening array arguments is that you can easily add conditions to your patches using nested arrays:

merge(state, [
  { week: 56 },
  state.age < 10 && { child: true },
  state.job === 'programmer' && [
    state.promote && { promoted: true },
    !state.salaryPaid && { schedulePayment: true }
  ]
])

If all the above conditions are false (except the job check) the final patch array before flattening will look like this:

patches === [{ week: 56 }, false, [false, false]]

Since falsy patches are ignored only { week: 56 } will be merged.

Another option is to use the spread operator to combine multiple patches into one, but it's harder/messier to write conditions using this technique as you can see:

merge(state, {
  { week: 56 },
  ...(state.age < 10 ? { child: true } : {}),
  ...(state.job === 'programmer'
    ? {
        ...(state.promote ? { promoted: true } : {}),
        ...(!state.salaryPaid ? { schedulePayment: true } : {})
      }
    : {})
})

As a reducer

Mergerino can be used as a reducer where patches are fed into a function which is then applied to a central state object. In these cases you may not have a reference to the full state object to base your patch on.

In order to help in this scenario mergerino supports passing a function as a top level patch. This function acts exactly the same as a function passed to a specific property. It receives the full state object as the first argument, the merge function as the second.

// state-manager.js
let state = { count: 10 }
const update = patch => (state = merge(state, patch))

// other.js
update({ newProp: true })
// want to use value of count to patch
update((state, m) => m(state, { double: state.count * 2 }))

// back in state-manager.js
console.log(state) // { count: 10, newProp: true, double: 20 }

// if you don't use the merge function the top level object will be replaced
update(state => ({}))
console.log(state) // {}

Credits

Check out patchinko by Barney Carroll. It was a huge inspiration for mergerino.

It takes a much more explicit approach to signaling patch operations and has some interesting API options.

mergerino's People

Contributors

foxdonut avatar fuzetsu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

mergerino's Issues

Handling of primitive values as the first argument

Hey there! Really cool util! I was pretty satisfied with Ramda, but mergerino combines several Ramda functions into one and then some. It reduces boilerplate code even more. Thank you for this util!

I'm considering integrating into my framework-agnostic state management library - actus (it's in early alpha stage).

I can see an edge case where mergerino could be improved. It's when the first argument is a primitive value.

These cases work as expected:

merge({a: 1}, {a: () => 2}); // {a: 2}
merge(1, () => 2); // 2

But these work inconsistently:

merge({a: 1}, {a: (previous) => previous + 1}); // {a: 2} (as expected)
merge([1], (previous) => [...previous, 2]); // [1, 2] (as expected)

merge(1, (previous) => previous + 1); // "[object Object]1" (expected to see 2)
merge({a: 1}, {a: 2}); // {a: 2} (as expected)

merge(1, 2); // {} (expected to see 2)

Same applies to other primitive types, like strings and booleans:

merge({a: false}, {a: (previous) => !previous}); // {a: true} (as expected)

merge(false, (previous) => !previous); // false (expected to see true)

I think the ability to handle primitive values as the first argument will make a more consistent behavior.

Webpack picks up "module" entry point instead of "main"

Hey, I don't know much about authoring node modules, so I'm not sure if there is anything you can do about this.
I'm using webpack to run my code through babel to get IE11 support.
I have it configured to only babelfy my source code, not node_modules.

If I import mergerino like import merge from 'mergerino'; webpack will pick up the ES6 version.
I can workaround it by instead doing import merge from 'mergerino/dist/mergerino.es5';.

webpack/webpack#5756

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.