Immuter
An immutable react/redux state update helper, easily handle nested state object with less code.
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
New Full-Static-Type-Safe Struct Feature!
Struct is a experiment feature, implemented by Proxy, highly inspired by monolite. I think you can call it an optimized version of deep clone, and works perfectly with React/Redux, have the same performance benefits of Facebook's immutable-js, but more natural, and Full-Static-Type-Safe support.
The most valuable part is it's clean API, write your code like we have language level support with immutable data. And because of this, it can work perfectly with both flow and ts!
It's using es6 Proxy internally, but don't worry, since the structure is fixed, we can using proxy-polyfill to support event IE 9!
Notice
You can only change struct's deep properties by straight call, other wise you might be changing the another struct in the clone chain, but there is no limitation for read. The reason is that the child Proxy instances are cached for shallow compare. e.g.
Thanks for @hjiayz's idea, we have a more safe interface, mutation should be called in the callback, and shouldn't access old struct.
// Best, thanksstruct = struct/* old struct */// okstruct = Struct// Not goodconst struct1 = Structstructabcd = ...struct1abcd = ...// Not goodconst struct1 = Structconst a = struct1ab = ...abcd = ...structa // read astructabcd = ...// Buggyconst struct1 = Structconst a = struct1structa // read aabcd = ... // `struct1.a` is strict equal with `struct.a`, so if you called `struct.a` before, a's context is set to `struct`, so you are modifying `struct.a` now, not `struct1.a`. I can do nothing to fix or warn you in the runtime, and since this is an immutable library, directly changing the original data is not recommended anyway.
If we don't want this limitation, we need to implement a specific compare function for struct data, and ===
won't work for cloned struct's child.
More Code Example:
let struct = const struct1 = struct // Clone struct, it will only change modified part to optimize performance. structauthor === 'J. k. rowling' // truestruct2author === 'New Author' // true Struct // true
Demo
Simple mutation method
// 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 titleEnlet bookLitelet newBook = book // get the English titletitleEn = Immuter// ortitleEn = Immuter// return: Harry Potter and the Philosopher\'s Stone // multiple getbookLite = Immuter// return {// title: 'Harry Potter and the Philosopher\'s Stone',// author: 'J. k. rowling',// type: 'book'// } // set the English titlenewBook = Immuter// ornewBook = Immuter// return: {// title: {// zh: '新标题!',// en: 'New title!',// },// author: 'J. k. rowling',// tags: ['novel', 'magic'],// } // set array itemnewBook = Immuter // 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// return: {// title: {// zh: '新标题!',// en: 'New title!',// },// author: 'J. k. rowling',// tags: ['New tag', 'magic', 'UK'],// } // multiple setnewBook = Immuter // multiple updatenewBook = Immuter // multiple deletenewBook = Immuter
Advance
bindObj
const book = title: zh: '哈利·波特与魔法石' en: 'Harry Potter and the Philosopher\'s Stone' author: 'J. k. rowling' tags: 'novel' 'magic'let newBook = bookconst immuBook = Immuterconst titleEn = immuBooknewBook = immuBook immuBooknewBook = immuBook
bindComp
Using bindComp decorator to bind a React Component, with flowtype.
type State = title: zh: string en: string author: string tags: Array<string> @Immuter 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' { this }
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