- Notifications
You must be signed in to change notification settings - Fork2
A state management library for React combined immutable, mutable and reactive mode
License
Lucifier129/bistate
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
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.
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
- ES2015 Proxy
- ES2015 Symbol
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
andtarget
, sharing the unchanged parts - we get two immutable states now
npm install --save bistate
yarn add bistate
importReactfrom'react'// import react-hooks api from bistate/reactimport{useBistate,useMutate}from'bistate/react'exportdefaultfunctionCounter(){// create state via useBistateletstate=useBistate({count:0})// safely mutate state via useMutateletincre=useMutate(()=>{state.count+=1})letdecre=useMutate(()=>{state.count-=1})return(<div><buttononClick={incre}>+1</button>{state.count}<buttononClick={decre}>-1</button></div>)}
functionTodo({ todo}){letedit=useBistate({value:false})/** * bistate text is reactive * we will pass the text down to TodoInput without the need of manually update it in Todo * */lettext=useBistate({value:''})// create a mutable function via useMutatelethandleEdit=useMutate(()=>{edit.value=!edit.valuetext.value=todo.content})lethandleEdited=useMutate(()=>{edit.value=falseif(text.value===''){// remove the todo from todos via remove functionremove(todo)}else{// mutate todo even it is not a local bistatetodo.content=text.value}})lethandleKeyUp=useMutate(event=>{if(event.key==='Enter'){handleEdited()}})lethandleRemove=useMutate(()=>{remove(todo)})lethandleToggle=useMutate(()=>{todo.completed=!todo.completed})return(<li><buttononClick={handleRemove}>remove</button><buttononClick={handleToggle}>{todo.completed ?'completed' :'active'}</button>{edit.value&&<TodoInputtext={text}onBlur={handleEdited}onKeyUp={handleKeyUp}/>}{!edit.value&&<spanonClick={handleEdit}>{todo.content}</span>}</li>)}functionTodoInput({ text, ...props}){lethandleChange=useMutate(event=>{/** * we just simply and safely mutate text at one place * instead of every parent components need to handle `onChange` event */text.value=event.target.value})return<inputtype="text"{...props}onChange={handleChange}value={text.value}/>}
import{createStore,mutate,remove,isBistate,debug,undebug}from'bistate'import{useBistate,useMutate,useBireducer,useComputed,useBinding,view,useAttr,useAttrs}from'bistate/react'
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.
letChild=(props:{counter?:{count:number}})=>{// if props.counter is existed, use props.counter, otherwise use local bistate.letstate=useBistate({count:0},props.counter)lethandleClick=useMutate(()=>{state.count+=1})return<divonClick={handleClick}>{state.count}</div>}// use local bistate<Child/>// use parent bistate<Childcounter={state}/>
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)
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'constTest=()=>{let[state,dispatch]=useBireducer((state,action)=>{if(action.type==='incre'){state.count+=1}if(action.type==='decre'){state.count-=1}},{count:0})lethandleIncre=()=>{dispatch({type:'incre'})}lethandleIncre=()=>{dispatch({type:'decre'})}// render view}
Create computed state
letstate=useBistate({first:'a',last:'b'})// use getter/setterletcomputed=useComputed({getvalue(){returnstate.first+' '+state.last},setvalue(name){let[first,last]=name.split(' ')state.first=firststate.last=last}},[state.first,state.last])lethandleEvent=useMutate(()=>{console.log(computed.value)// 'a b'// updatecomputed.value='Bill Gates'console.log(state.first)// Billconsole.log(state.last)// Gates})
Create binding state
A binding state is an object has only one filed{ value }
letstate=useBistate({text:'some text'})let{ text}=useBinding(state)// don't do this// access field will trigger a react-hooks// you should always use ECMAScript 6 (ES2015) destructuring to get binding stateletbindingState=useBinding(state)if(xxx)xxx=bindingState.xxxlethandleChange=()=>{console.log(text.value)// some textconsole.log(state.text)// some texttext.value='some new text'console.log(text.value)// some new textconsole.log(state.text)// some new text}
It's useful when child component needs binding state, but parent component state is not.
functionInput({ text, ...props}){lethandleChange=useMutate(event=>{/** * we just simply and safely mutate text at one place * instead of every parent components need to handle `onChange` event */text.value=event.target.value})return<inputtype="text"{...props}onChange={handleChange}value={text.value}/>}functionApp(){letstate=useBistate({fieldA:'A',fieldB:'B',fieldC:'C'})let{ fieldA, fieldB, fieldC}=useBinding(state)return<><Inputtext={fieldA}/><Inputtext={fieldB}/><Inputtext={fieldC}/></>}
create a two-way data binding function-component
constCounter=view(props=>{// Counter will not know the count is local or came from the parentletcount=useAttr('count',{value:0})lethandleClick=useMutate(()=>{count.value+=1})return<buttononClick={handleClick}>{count.value}</button>})// use local bistate<Counter/>// create a two-way data binding connection with parent bistate<Countcount={parentBistate.count}/>
create a record of bistate, when the value in props[key] is bistate, connect it.
useAttrs must use in view(fc)
constTest=view(()=>{// Counter will not know the count is local or came from the parentletattrs=useAttrs({count:{value:0}})lethandleClick=useMutate(()=>{attrs.count.value+=1})return<buttononClick={handleClick}>{attrs.count.value}</button>})// use local bistate<Counter/>// create a two-way data binding connection with parent bistate<Countcount={parentBistate.count}/>
a shortcut ofuseAttrs({ [key]: initValue })[key]
, it's useful when we want to separate attrs
create a store with an initial state
subscribe to the store, and return an unlisten function
Every time the state has been mutated, a new state will publish to every listener.
get the current state in the store
letstore=createStore({count:1})letstate=store.getState()letunlisten=store.subscribe(nextState=>{expect(state).toEqual({count:1})expect(nextState).toEqual({count:2})unlisten()})mutate(()=>{state.count+=1})
immediately execute the function and return the value
it's free to mutate the bistate in mutate function
remove the bistate from its parent
check if input is a bistate or not
enable debug mode, break point when bistate is mutating
disable debug mode
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
letstate=useBistate([{value:1}])mutate(()=>{state.push(state[0])// nextState[0] is equal to state[0]// nextState[1] is not equal to state[0], it's a new one})
- can not spread object or array as props, it will lose the reactivity connection in it, should pass the reference
// don't do this<Todo{...todo}/>// do this instead<Todotodo={todo}/>
can not edit state or props via react-devtools, the same problem as above
useMutate or mutate do not support async function
constTest=()=>{letstate=useBistate({count:0})// don't do thislethandleIncre=useMutate(async()=>{letn=awaitfetchData()state.count+=n})// do this insteadletincre=useMutate(n=>{state.count+=n})lethandleIncre=async()=>{letn=awaitfetchData()incre(n)}return<divonClick={handleIncre}>test</div>}
👤Jade Gu
- Twitter:@guyingjie129
- Github:@Lucifier129
Contributions, issues and feature requests are welcome!
Feel free to checkissues page.
Give a ⭐️ if this project helped you!
Copyright © 2019Jade Gu.
This project isMIT licensed.
This README was generated with ❤️ byreadme-md-generator