modify-via-query
v1.0.7
Published
Modify an object using query without mutating the original object.
Downloads
9
Maintainers
Readme
modify-via-query
Mutate a copy of data without changing the original source with natural and type-safe query.
Why use this library?
This is for users that are sick of updating large states. For example:
setState((state) => ({
...state,
book: {
...state.book,
author: {
...state.book.author,
nickNames: state.book.author.map((name, index) =>
index === targetIndex ? "new name" : name
),
},
},
}));
With this library, the code above can be simplified as:
setState(
modify((state) => state
.book
.author
.nickNames[targetIndex]
.$set("new name")
)
);
How to install?
Node.js
npm install modify-via-query --save
import {modify} from "modify-via-query"
Deno
import { modify } from "https://raw.githubusercontent.com/wongjiahau/modify-via-query/master/mod.ts";
Comparison with immutability-helper
Using immutability-helper, taken from this issue:
update(state, {
objects: {
[resource]: {
[id]: {
relationships: {
[action.relationship]: {
data: {
$apply: data => {
const { id, type } = response.data;
const ref = { id, type };
return data == null ? [ref] : [...data, ref];
}
}
}
}
}
}
}
});
Using modify-via-query:
modify(state => state
.objects[resource][id]
.relationships[action.relationship]
.data
.$apply(data => {
const { id, type } = response.data;
const ref = { id, type };
return data == null ? [ref] : [...data, ref];
})
)(state)
Features
- Type-safe
- Autocomplete via Intellisense
- Chainable
- Immutable
- Shallow copy
Main concept
Like the name of this package, you modify by querying the property.
The modify
function make the object modifiable. A modifiable object comes with a few commands like $set
and $apply
.
Basically, the commands can be access in any hierarchy of the object, and once the command is invoked, an updated modifiable object will be returned, such that more modifications can be chained.
Examples
Modify object
modify(state => state.x.$set(3))({ x: 2 }) // {x: 3}
Modify array item
modify(state => state[0].$set(3))([1, 2]) // [3, 2]
Modify nested object array
modify(state => state.todos[0].done.$apply(done => !done))({
todos: [
{content: "code", done: false},
{content: "sleep", done: false},
]
})
// {todos: [{content: "code", done: true}, {content: "sleep", done: false}]}
Chaining commands
modify(state => state
.name.$apply(name => name + " " + "squarepants")
.job.at.$set("Krabby Patty")
)({
name: "spongebob",
job: {
title: "chef"
at: undefined
}
})
// { name: "spongebob squarepants", job: {title: "chef", at: "Krabby Patty"} }
Removing array item
modify(state => state.filter((_, index) => index !== 2))(
["a", "b", "c"]
)
// ["a", "b"]
Modify property of optional object
For example, if you have the following state:
const state: {
pet?: {
name: string
age?: number
}
} = {}
Let say you want to update pet.age
, you cannot do this:
modify(state => state.pet.age.$set(9))(state)
You will get compile-error by doing so. The is prohibited in order to maintain the type consistency, else the resulting value would be {pet: {age: 9}}
, which breaks the type of state
, because name
should be present.
To fix this, you have to provide a default value for pet
using the $default
command:
modify(state => state.pet.$default({name: "bibi"}).age.$set(9))(state)
This tells the library that if pet
is undefined, then its name will be "bibi"
otherwise the original name will be used.
Available commands
$set
- to set the value of the queried property
$apply
- to update the value of the queried property based on its previous value
$default
- to provide a default value if the queried property is a nullable object
Usage with React
Function components (useState hook)
const Counter = () => {
const [state, setState] = React.useState({count: 0})
const add = () => setState(modify(state => state.count.$apply(x => x + 1)))
const minus = () => setState(modify(state => state.count.$apply(x => x - 1)))
return (...)
}
Class components
class Counter extends React.Component<{}, {count: 0}> {
constructor(props) => {
super(props)
this.state = {count: 0}
}
add = () => {
this.setState(modify(state => state.count.$apply(x => x + 1)))
}
minus = () => {
this.setState(modify(state => state.count.$apply(x => x - 1)))
}
}
Redux reducers
type State = {count: 0}
type Action = {type: 'add'} | {type: 'minus'}
const myReducer = (state: State, action: Action): State => {
return modify(state)(state => {
switch(action.type) {
case 'add':
return state.count.$apply(x => x + 1)
case 'minus':
return state.count.$apply(x => x - 1)
}
})
}
Can I use this library in non-React projects?
Yes. Although this library is primarily for users who uses React users, this package can actually be used anywhere since it has zero dependency.
Can I use this library with Typescript?
Yes! In fact the default package already contain the type definitions, so you don't have to install it somewhere else.
How this library works?
It works by using Proxy
Overloads
The modify
function is overloaded with two signatures. If you are using React, the first variant will be more convenient. Note that both of the variants can be curried.
// Update -> State -> State
modify: (update: (state: Modifiable<State>) => Modifiable<State>)
=> (state: State)
=> State;
// State -> Update -> State
modify: (state: State)
=> (update: (state: Modifiable<State>) => Modifiable<State>)
=> State;
References
This library is inspired by: