npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

immuter

v2.0.5

Published

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

Downloads

58

Readme

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

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, thanks
struct = struct/* old struct */.clone((struct /* cloned struct*/) => {
  struct.a.b.c.d = ...
  struct.b.a.c = ...
  const { a } = struct
  a.c.d = ...
  struct.a.c.d.e = ...
  // return struct // you can return the struct or not, if you return we will use return first.
})
// ok
struct = Struct.clone(struct /* old struct */, (struct /* cloned struct*/) => {
  const { a } = struct
  a.c.d = ...
  struct.a.c.d.e = ...
})
// Not good
const struct1 = Struct.clone(struct)
struct.a.b.c.d = ...
struct1.a.b.c.d = ...
// Not good
const struct1 = Struct.clone(struct)
const { a } = struct1
a.b = ...
a.b.c.d = ...
struct.a // read a
struct.a.b.c.d = ...
// Buggy
const struct1 = Struct.clone(struct)
const { a } = struct1
struct.a // read a
a.b.c.d = ... // `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:

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