Skip to content

zaaack/immuter

Repository files navigation

Immuter

An immutable react/redux state update helper, easily handle nested state object with less code.

Build Status npm npm

Why

Facebook's Immutable.js is too heavy, seamless-immutable is lite and simple, and backwards-compatible with normal Arrays and Objects. But the way to update is not friendly enough for me, I have to write too much code for updating state. I also tried something like dot-prop-immutable, object-path-immutable, timm, updeep, update-immutable, etc. they all are good, but neither of them's DX is good enough for me, so I create this one based on all benefits of these.

Install

npm i immuter

# or

yarn add immuter

More Code Example:

import { Struct } from 'immuter'

let struct = Struct({
  title: {
    zh: '哈利·波特与魔法石',
    en: 'Harry Potter and the Philosopher\'s Stone',
  },
  author: 'J. k. rowling',
  tags: ['novel', 'magic'],
})

const struct1 = struct.clone(struct => {
  struct.author = 'New Author'
  struct.title.en = 'New Title'
  // return struct // return or not
}) // Clone struct, it will only change modified part to optimize performance.

struct.author === 'J. k. rowling' // true
struct2.author === 'New Author' // true

Struct.isStruct(struct) // true

Demo

Simple mutation method

import Immuter from 'immuter'
// or import { bindObj, binComp, get, set, update, del } from 'immuter'
const book = {
  title: {
    zh: '哈利·波特与魔法石',
    en: 'Harry Potter and the Philosopher\'s Stone',
  },
  author: 'J. k. rowling',
  tags: ['novel', 'magic'],
}

let titleEn
let bookLite
let newBook = book


// get the English title
titleEn = Immuter.get(book, 'title.en')
// or
titleEn = Immuter.get(book, ['title', 'en'])
// return: Harry Potter and the Philosopher\'s Stone

// multiple get
bookLite = Immuter.get(book, {
  'title': 'title.en',
  'author': 'author',
}, {
  'type': 'book',
})
// return {
//  title: 'Harry Potter and the Philosopher\'s Stone',
//  author: 'J. k. rowling',
//  type: 'book'
// }

// set the English title
newBook = Immuter.set(newBook, 'title.zh', '新标题!')
// or
newBook = Immuter.set(newBook, ['title', 'en'], 'New title!')
// return:  {
//   title: {
//     zh: '新标题!',
//     en: 'New title!',
//   },
//   author: 'J. k. rowling',
//   tags: ['novel', 'magic'],
// }


// set array item
newBook = Immuter.set(newBook, 'tags[0]', 'New tag')

// update array, update is almost like the set, except the value is a function to update value,
// note this function should be pure!
newBook = Immuter.update(book, 'tags', tags => tags.concat(['UK']))
// return:  {
//   title: {
//     zh: '新标题!',
//     en: 'New title!',
//   },
//   author: 'J. k. rowling',
//   tags: ['New tag', 'magic', 'UK'],
// }


// multiple set
newBook = Immuter.set(newBook, {
  'title.en': 'New Title!',
  'author': 'New Author!'
})


// multiple update
newBook = Immuter.update(newBook, {
  'title.en': title => title + ' (Original Edition)',
  'author': author => author.toUpperCase(),
  'tags': tags => tags.concat(['UK']),
})

// multiple delete
newBook = Immuter.delete(newBook, {
  'title.zh': true, // this would be removed
  'author': false, // this won't
  'tags': false, // this won't, too
})

Advance

bindObj

import Immuter from 'immuter'

const book = {
  title: {
    zh: '哈利·波特与魔法石',
    en: 'Harry Potter and the Philosopher\'s Stone',
  },
  author: 'J. k. rowling',
  tags: ['novel', 'magic'],
}
let newBook = book
const immuBook = Immuter.bindObj(newBook)
const titleEn = immuBook.get('title.en')
newBook = immuBook.set('title.en', 'New title!')

immuBook.set('author', 'J.K')
newBook = immuBook.getObj()

bindComp

Using bindComp decorator to bind a React Component, with flowtype.

import { Component } from 'react'
import Immuter from 'immuter'
import type { ImmuterGet, ImmuterSet, ImmuterUpdate, ImmuterDel } from 'immuter'

type State = {
  title: {
    zh: string,
    en: string,
  },
  author: string,
  tags: Array<string>,
}

@Immuter.bindComp()
class CompA extends Component {
  get: ImmuterGet<State>
  set: ImmuterSet<State>
  update: ImmuterUpdate<State>
  del: ImmuterDel<State>
  delete: ImmuterDel<State>
  state: State = {
    title: {
      zh: '哈利·波特与魔法石',
      en: 'Harry Potter and the Philosopher\'s Stone',
    },
    author: 'J. k. rowling',
    tags: ['novel', 'magic'],
  }

  componentDidMount() {
    this.update('title.en', title => title + ' (Original Edition)')
      .then((state) => {
        // do what you want in setState callback
      })
  }
}

API

Immuter.get: <T: Object>(obj: T, string | Array, defaults: any) => any

Get a deep property by dot path or array path

Note: get wouldn't deep clone result for performance issues, just make sure all your modify operations are using immuter :).

Immuter.get<T: Object>(obj: T, path: { [string]: string | Array }, defaults: { [string]: any }) => { [string]: any }

Get deep properties by an Object with custom key.

Immuter.set<T: Object>(obj: T, string | Array, value: any) => T

Set a deep property by dot path or array path

Immuter.set<T: Object>(obj: T, pathValueMap: { [string | Array]: any }) => T

Set deep properties by an Object with Path key

Immuter.update<T: Object>(obj: T, string | Array, updater: (val: any) => any) => T

Mostly like set, except passing a function to update value

Immuter.update<T: Object>(obj: T, pathUpdaterMap: { [string | Array]: (val: any) => any }) => T

Multi update with an path: updater map

Immuter.bindObj<T: Object>(obj: T, chain: boolean = false): ImmuterWrapper

This function will return an ImmuterWrapper instance with all functions above as it's methods, and bind the obj inside, so you don't need to pass obj.

  • chain: default false, modify method would return the modified object directly, otherwise would return this for chained calls.

Immuter.bindComp<T: Object>(ns: string | boolean=false, includes?: ?Array, excludes: Array = ['bindObj', 'bindComp'])

This function will bind immuter functions to React Component instance, you can get, set, delete or update component state directly with instance method get, set, delete or update.

  • ns: Whether using namespace, defaults is false, means immuter functions would mount on component instance, you can call this.get('title.en'), this.set('title.en', 'Some title'), etc. in your component. Or using an special object to mount, e.g ns='immter', so you should call like this: this.get('title.en'), this.set('title.en', 'Some title')
  • includes: An array of include methods, defaults is all.
  • excludes: An array of exclude methods, defaults is ['bindObj', 'bindComp'].

These methods will auto update state by this.setState, if you need to using setState's callback feature, don't worry, all modify methods will return a promise, so you can even using async/await with it!

Exported flow types for bindComp

export type ImmuterGet = (path: GetPath, defaults: *) => *
export type ImmuterSet = <State>(path: SetPath, value: *) => State
export type ImmuterUpdate = <State>(path: UpdatePath, fn?: Updater) => State
export type ImmuterDel = <State>(path: DelPath) => State

Licence MIT