Welcome to bistate 👋
Create the next immutable state tree by simply modifying the current tree
bistate is a tiny package that allows you to work with the immutable state in a more mutable and reactive way, inspired by vue 3.0 reactivity API and immer.
Benefits
bistate is like immer but more reactive
- Immutability with normal JavaScript objects and arrays. No new APIs to learn!
- Strongly typed, no string based paths selectors etc.
- Structural sharing out of the box
- Deep updates are a breeze
- Boilerplate reduction. Less noise, more concise code.
- Provide react-hooks API
- Small size
- Reactive
Environment Requirement
- ES2015 Proxy
- ES2015 Symbol
Can I Use Proxy?
How it works
Every immutable state is wrapped by a proxy, has a scapegoat state by the side.
immutable state
+ scapegoat state
= bistate
- the immutable target is freezed by proxy
- scapegoat has the same value as the immutable target
- mutate(() => { the_mutable_world }), when calling
mutate(f)
, it will
- switch all operations to scapegoat instead of the immutable target when executing
- switch back to the immutable target after executed
- create the next bistate via
scapegoat
and target
, sharing the unchanged parts - we get two immutable states now
Install
npm install --save bistate
yarn add bistate
Usage
Counter
import React from 'react'
import { useBistate, useMutate } from 'bistate/react'
export default function Counter() {
let state = useBistate({ count: 0 })
let incre = useMutate(() => {
state.count += 1
})
let decre = useMutate(() => {
state.count -= 1
})
return (
<div>
<button onClick={incre}>+1</button>
{state.count}
<button onClick={decre}>-1</button>
</div>
)
}
TodoApp
function Todo({ todo }) {
let edit = useBistate({ value: false })
let text = useBistate({ value: '' })
let handleEdit = useMutate(() => {
edit.value = !edit.value
text.value = todo.content
})
let handleEdited = useMutate(() => {
edit.value = false
if (text.value === '') {
remove(todo)
} else {
todo.content = text.value
}
})
let handleKeyUp = useMutate(event => {
if (event.key === 'Enter') {
handleEdited()
}
})
let handleRemove = useMutate(() => {
remove(todo)
})
let handleToggle = useMutate(() => {
todo.completed = !todo.completed
})
return (
<li>
<button onClick={handleRemove}>remove</button>
<button onClick={handleToggle}>{todo.completed ? 'completed' : 'active'}</button>
{edit.value && <TodoInput text={text} onBlur={handleEdited} onKeyUp={handleKeyUp} />}
{!edit.value && <span onClick={handleEdit}>{todo.content}</span>}
</li>
)
}
function TodoInput({ text, ...props }) {
let handleChange = useMutate(event => {
text.value = event.target.value
})
return <input type="text" {...props} onChange={handleChange} value={text.value} />
}
API
import { createStore, mutate, remove, isBistate, debug, undebug } from 'bistate'
import {
useBistate,
useMutate,
useBireducer,
useComputed,
useBinding,
view,
useAttr,
useAttrs
} from 'bistate/react'
useBistate(array | object, bistate?) -> bistate
receive an array or an object, return bistate.
if the second argument is another bistate which has the same shape with the first argument, return the second argument instead.
let Child = (props: { counter?: { count: number } }) => {
let state = useBistate({ count: 0 }, props.counter)
let handleClick = useMutate(() => {
state.count += 1
})
return <div onClick={handleClick}>{state.count}</div>
}
<Child />
<Child counter={state} />
useMutate((...args) => any_value) -> ((...args) => any_value)
receive a function as argument, return the mutable_function
it's free to mutate any bistates in mutable_function, not matter where they came from(they can belong to the parent component)
useBireducer(reducer, initialState) -> [state, dispatch]
receive a reducer and an initial state, return a pair [state, dispatch]
its' free to mutate any bistates in the reducer funciton
import { useBireducer } from 'bistate/react'
const Test = () => {
let [state, dispatch] = useBireducer(
(state, action) => {
if (action.type === 'incre') {
state.count += 1
}
if (action.type === 'decre') {
state.count -= 1
}
},
{ count: 0 }
)
let handleIncre = () => {
dispatch({ type: 'incre' })
}
let handleIncre = () => {
dispatch({ type: 'decre' })
}
}
useComputed(obj, deps) -> obj
Create computed state
let state = useBistate({ first: 'a', last: 'b' })
let computed = useComputed({
get value() {
return state.first + ' ' + state.last
},
set value(name) {
let [first, last] = name.split(' ')
state.first = first
state.last = last
}
}, [state.first, state.last])
let handleEvent = useMutate(() => {
console.log(computed.value)
computed.value = 'Bill Gates'
console.log(state.first)
console.log(state.last)
})
useBinding(bistate) -> obj
Create binding state
A binding state is an object has only one filed { value }
let state = useBistate({ text: 'some text' })
let { text } = useBinding(state)
let bindingState = useBinding(state)
if (xxx) xxx = bindingState.xxx
let handleChange = () => {
console.log(text.value)
console.log(state.text)
text.value = 'some new text'
console.log(text.value)
console.log(state.text)
}
It's useful when child component needs binding state, but parent component state is not.
function Input({ text, ...props }) {
let handleChange = useMutate(event => {
text.value = event.target.value
})
return <input type="text" {...props} onChange={handleChange} value={text.value} />
}
function App() {
let state = useBistate({
fieldA: 'A',
fieldB: 'B',
fieldC: 'C'
})
let { fieldA, fieldB, fieldC } = useBinding(state)
return <>
<Input text={fieldA} />
<Input text={fieldB} />
<Input text={fieldC} />
</>
}
view(FC) -> FC
create a two-way data binding function-component
const Counter = view(props => {
let count = useAttr('count', { value: 0 })
let handleClick = useMutate(() => {
count.value += 1
})
return <button onClick={handleClick}>{count.value}</button>
})
<Counter />
<Count count={parentBistate.count} />
useAttrs(initValue) -> Record<string, bistate>
create a record of bistate, when the value in props[key] is bistate, connect it.
useAttrs must use in view(fc)
const Test = view(() => {
let attrs = useAttrs({ count: { value: 0 } })
let handleClick = useMutate(() => {
attrs.count.value += 1
})
return <button onClick={handleClick}>{attrs.count.value}</button>
})
<Counter />
<Count count={parentBistate.count} />
useAttr(key, initValue) -> bistate
a shortcut of useAttrs({ [key]: initValue })[key]
, it's useful when we want to separate attrs
createStore(initialState) -> { subscribe, getState }
create a store with an initial state
store.subscribe(listener) -> unlisten
subscribe to the store, and return an unlisten function
Every time the state has been mutated, a new state will publish to every listener.
store.getState() -> state
get the current state in the store
let store = createStore({ count: 1 })
let state = store.getState()
let unlisten = store.subscribe(nextState => {
expect(state).toEqual({ count: 1 })
expect(nextState).toEqual({ count: 2 })
unlisten()
})
mutate(() => {
state.count += 1
})
mutate(f) -> value_returned_by_f
immediately execute the function and return the value
it's free to mutate the bistate in mutate function
remove(bistate) -> void
remove the bistate from its parent
isBistate(input) -> boolean
check if input is a bistate or not
debug(bistate) -> void
enable debug mode, break point when bistate is mutating
undebug(bistate) -> void
disable debug mode
Caveats
-
only supports array and object, other data types are not allowed
-
bistate is unidirectional, any object or array appear only once, no circular references existed
let state = useBistate([{ value: 1 }])
mutate(() => {
state.push(state[0])
})
- can not spread object or array as props, it will lose the reactivity connection in it, should pass the reference
<Todo {...todo} />
<Todo todo={todo} />
-
can not edit state or props via react-devtools, the same problem as above
-
useMutate or mutate do not support async function
const Test = () => {
let state = useBistate({ count: 0 })
let handleIncre = useMutate(async () => {
let n = await fetchData()
state.count += n
})
let incre = useMutate(n => {
state.count += n
})
let handleIncre = async () => {
let n = await fetchData()
incre(n)
}
return <div onClick={handleIncre}>test</div>
}
Author
👤 Jade Gu
🤝 Contributing
Contributions, issues and feature requests are welcome!
Feel free to check issues page.
Show your support
Give a ⭐️ if this project helped you!
📝 License
Copyright © 2019 Jade Gu.
This project is MIT licensed.
This README was generated with ❤️ by readme-md-generator