Natural and type-safe query to mutate a copy of data without changing the original source
MIT License
Mutate a copy of data without changing the original source with natural and type-safe query.
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")
)
);
npm install modify-via-query --save
import {modify} from "modify-via-query"
import { modify } from "https://raw.githubusercontent.com/wongjiahau/modify-via-query/master/mod.ts";
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)
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.
modify(state => state.x.$set(3))({ x: 2 }) // {x: 3}
modify(state => state[0].$set(3))([1, 2]) // [3, 2]
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}]}
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"} }
modify(state => state.filter((_, index) => index !== 2))(
["a", "b", "c"]
)
// ["a", "b"]
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.
$set
$apply
$default
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 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)))
}
}
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)
}
})
}
Yes. Although this library is primarily for users who uses React users, this package can actually be used anywhere since it has zero dependency.
Yes! In fact the default package already contain the type definitions, so you don't have to install it somewhere else.
It works by using Proxy
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;
This library is inspired by: