A state management library for React combined immutable, mutable and reactive mode

Overview

Welcome to bistate πŸ‘‹

npm version Build Status Documentation Maintenance License: MIT Twitter: guyingjie129

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.

🏠 Homepage

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 react-hooks api from bistate/react
import { useBistate, useMutate } from 'bistate/react'

export default function Counter() {
  // create state via useBistate
  let state = useBistate({ count: 0 })

  // safely mutate state via useMutate
  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 })
  /**
   * bistate text is reactive
   * we will pass the text down to TodoInput without the need of manually update it in Todo
   * */
  let text = useBistate({ value: '' })

  // create a mutable function via useMutate
  let handleEdit = useMutate(() => {
    edit.value = !edit.value
    text.value = todo.content
  })

  let handleEdited = useMutate(() => {
    edit.value = false
    if (text.value === '') {
      // remove the todo from todos via remove function
      remove(todo)
    } else {
      // mutate todo even it is not a local bistate
      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 => {
    /**
     * 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 <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 } }) => {
  // if props.counter is existed, use props.counter, otherwise use local bistate.
  let state = useBistate({ count: 0 }, props.counter)

  let handleClick = useMutate(() => {
    state.count += 1
  })

  return <div onClick={handleClick}>{state.count}</div>
}

// use local bistate
<Child />
// use parent bistate
<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' })
  }

  // render view
}

useComputed(obj, deps) -> obj

Create computed state

let state = useBistate({ first: 'a', last: 'b' })

// use getter/setter
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) // 'a b'
  // update
  computed.value = 'Bill Gates'

  console.log(state.first) // Bill
  console.log(state.last) // Gates
})

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)

// don't do this
// access field will trigger a react-hooks
// you should always use ECMAScript 6 (ES2015) destructuring to get binding state
let bindingState = useBinding(state)
if (xxx) xxx = bindingState.xxx

let handleChange = () => {
  console.log(text.value) // some text
  console.log(state.text) // some text
  text.value = 'some new text'
  console.log(text.value) // some new text
  console.log(state.text) // some new text
}

It's useful when child component needs binding state, but parent component state is not.

function Input({ text, ...props }) {
  let handleChange = 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 <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 => {
  // Counter will not know the count is local or came from the parent
  let count = useAttr('count', { value: 0 })

  let handleClick = useMutate(() => {
    count.value += 1
  })

  return <button onClick={handleClick}>{count.value}</button>
})

// use local bistate
<Counter />

// create a two-way data binding connection with parent bistate
<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(() => {
  // Counter will not know the count is local or came from the parent
  let attrs = useAttrs({ count: { value: 0 } })

  let handleClick = useMutate(() => {
    attrs.count.value += 1
  })

  return <button onClick={handleClick}>{attrs.count.value}</button>
})

// use local bistate
<Counter />

// create a two-way data binding connection with parent bistate
<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])
  // 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
<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 })

  // don't do this
  let handleIncre = useMutate(async () => {
    let n = await fetchData()
    state.count += n
  })

  // do this instead
  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

You might also like...
A very simple but powerful state management for vuejs projects.
A very simple but powerful state management for vuejs projects.

Vuez A Simple but Powerful State Management for Vue.js projects. Vuez is a very simple but powerful state management library for Vue.js projects. Vuez

State management system for Vue.js

Vue States Vue States is a state management system for Vue.js. Checkout the examples at https://github.com/JohannesLamberts/vue-states-examples. You m

Local state management within Vuex

vuex-local Local state management within Vuex Why? Global state management is one of the problems on huge application development. Developers address

Simplify vuex loading state management

vuex-loading Simplify vuex loading state management Installing Using npm: $ npm install vuex-loadings -s Know Simplify vuex loading state management n

A vue boiler plate with state management, vuex, vue-router that can be backed by a laravel restful api using jwt auth
A vue boiler plate with state management, vuex, vue-router that can be backed by a laravel restful api using jwt auth

Laravel 6 (LTS) Vue.js Frontend Boilerplate A Vue.js Frontend starter project kit template/boilerplate with Laravel 6 Backend API support. Features Re

A find Coach is a vue state management project that allows users to find coach to mentor a student

A find Coach is a vue state management project that allows users to find coach to mentor a student. A simple vue app with routing functionality and vuex management for solid SPA

An implementation of the Mobx/Vue state tracking approach, for library authors

proxy-state-tree An implementation of the Mobx/Vue state tracking approach, for library authors DEPRECATED Has moved to repo: https://github.com/cereb

Easily share reactive data between your Vue components.

vue-stash A Vue.js plugin that makes it easy to share reactive data between components. This plugin is best suited for the rapid development of protot

A project to demonstrate the use of Store and state, getters, mutations in Vuex and VueJS

Vuex State Sample 🎁 🎯 A project to demonstrate the use of Store and state, getters, mutations in Vuex and VueJS. Create Vuex Store and use it to hol

Comments
  • Value Unwrapping and Wrapping.

    Value Unwrapping and Wrapping.

    In Vue 3.0 function based api, atomic type value and state are handled different. When nest a value in a state, the value is unwrapped.

    const count = value(0)
    const obj = state({
      count
    })
    
    console.log(obj.count) // 0
    
    obj.count++
    console.log(obj.count) // 1
    console.log(count.value) // 1
    

    But in Bistate, there's just state. So when nested, it's not unwrapped.

    const count = useBistate({ value: 0 })
    const obj = state({
      count
    })
    
    console.log(obj.count) // expected 0 but got { value: 0 }
    

    Besides, when passing a value to another component, it should be wrapped in a field such as value. Can Bistate automatically wrap an atomic type to a value, so we can make the state more clear and readable?

    function Input({ value }) {
      const onChange = useMutate(e => {
        value.value = e.target.value;
      })
      return <input value={value.value} onChange={onChange} />
    }
    
    function App() {
      const state = useBistate({
        foo: { value: 'some value' }
      })  // expect { foo: 'some value' } but must be { foo: { value: 'some value' } }
      return <Input value={state.foo} />
    }
    
    opened by littlehaker 17
Owner
ε·₯业聚
Thinking, coding and writing for fun
ε·₯业聚
Lightweight vuex inspired centralized state management library for all kinds of javascript applications. Great for React Native.

Verx Lightweight vuex inspired centralized state management library for all kinds of javascript applications. install npm install --save verx # yarn a

Jaynti Kanani 3 Nov 4, 2019
Type-safe reactive state management

rxsv Framework agnostic minimal state management library based on RxJS, heavily inspired by Redux and Redux-Observable with limited boilerplate and Ty

Grzegorz Bielski 22 Jun 28, 2022
State Management made eXtraordinarily simple and effective for Angular, React, and Vue

XSM - State Management made eXtraordinarily simple and effective for Angular, React, Vue, and Svelte. ?? Homepage Demos Angular React Svelte Vue Realw

Peter Lu 138 Sep 21, 2022
A tiny (198 bytes) state manager for React/RN/Preact/Vue/Svelte with many atomic tree-shakable stores

A tiny (198 bytes) state manager for React/RN/Preact/Vue/Svelte with many atomic tree-shakable stores

Nano Stores 2.2k Dec 27, 2022
:rabbit2: A tiny, light and handy state management for vuejs 2, writing less verbose code.

revuejs ?? A tiny, light and handy state management for vuejs 2, writing less verbose code. Installation Install the pkg with npm: npm install revuejs

Pomy 24 May 5, 2021
A light and easy shared state management plugin for Vue

vue-shared vue-shared is a tiny (~150 lines) vue plugin for shared state management, that can be used as an alternative to Vuex. It adds a new vue opt

null 11 Jan 22, 2021
Simple, unopinionated, lightweight and extensible state management for Vue 3

Simple, unopinionated, lightweight and extensible state management for Vue 3

Andrew Courtice 466 Dec 30, 2022
Simple counter with Vue.js and Vuex as state management

vuex-counter Project setup npm install Compiles and hot-reloads for development npm run serve Compiles and minifies for production npm run build Li

Kevin Hamdajani 0 Dec 30, 2021
πŸ—ƒοΈ Centralized State Management for Vue.js.

Vuex ?? HEADS UP! You're currently looking at Vuex 3 branch. If you're looking for Vuex 4, please check out 4.0 branch. Vuex is a state management pat

vuejs 27.9k Dec 30, 2022
Elm-inspired Application State Management for Vue.js.

VuElm It's a Vue state management inspired by Elm architecture. Concepts There are basically four elements on Elm architecture: Model, Actions, Update

Keuller MagalhΓ£es 36 May 5, 2021