๐Ÿ”ฎ tiny robust state management

Related tags

React snapstate
Overview

๐Ÿ”ฎ snapstate

tiny robust state management

๐Ÿ“ฆ npm install @chasemoskal/snapstate

๐Ÿ‘๏ธ watch for changes to properties
๐Ÿ•ต๏ธ track only the properties you are reading, automatically
โ™ป๏ธ keeps you safe from circular updates
โ›น๏ธ updates are debounced, avoiding duplicate updates
๐ŸŒณ carve large state trees into substates
๐Ÿงฌ implemented with recursive es proxies
๐Ÿ”ฌ typescript-native types, es modules
๐Ÿ’– free and open source, just for you

snapstate is designed to be a modern replacement for mobx. mobx was amazing, but has grown comically large at like 50 KB. mobx is also global, among other complications that we don't prefer.

snapstate is 1.5 KB, minified and gzipped.


using snapstate


๐Ÿ’พ readable and writable

  • the first cool thing about snapstate, is that it separates the readable and writable access to your state. you'll soon learn why that's rad.
  • let's create some state.
    import {snapstate} from "@chasemoskal/snapstate"
    
    const state = snapstate({
      count: 0,
      coolmode: "enabled",
    })
  • we can read the state's properties via readable.
    console.log(state.readable.count)
     // 0
  • but, importantly, readable won't let us write properties.
    state.readable.count += 1
     // SnapstateReadonlyError -- no way, bucko!
  • instead, we write properties via writable.
    state.writable.count += 1
     // this is allowed
    
    console.log(state.readable.count)
     // 1
  • this separation is great, because we can pass readable to parts of our application that should not be allowed to change the state. this is how we control access.
  • this makes it easy to formalize actions. it's as easy as giving our action functions access to the writable state.
    function myIncrementAction() {
      state.writable.count += 1
    }
  • then we might give our frontend components the state.readable, and state.track functions.
  • this makes it easy to achieve a clean uni-directional dataflow for our application's state.
  • writable is, itself, also readable.
    console.log(state.writable.count)
     // 1

๐Ÿ•ต๏ธ tracking changes

  • we can track changes to the properties we care about.
    const state = snapstate({count: 0, coolmode: "enabled"})
    
    state.track(() => {
      console.log(`count changed: ${state.readable.count}`)
        //                                         โ˜๏ธ
        //               snapstate detects this property read,
        //               and will run this tracker function
        //               whenever the property changes.
    })
     // 0 -- runs once initially
    
    state.writable.count += 1
     // 1 -- automatically runs the relevant tracker functions
    
    state.writable.coolmode = "disabled"
     // ~ nothing is logged to console ~
     // our track callback doesn't care about this property
  • we can be more pedantic, with a custom tracker, to avoid the initial run.
    const state = snapstate({count: 0, coolmode: "enabled"})
    
    state.track(
    
      // observer: listen specifically to "count"
      ({count}) => ({count}),
    
      // reaction: responding to changes
      ({count}) => console.log(`count changed: ${count}`),
    )
    
    state.writable.count += 1
     // 1
  • we can also stop tracking things when we want.
    const state = snapstate({count: 0, coolmode: "enabled"})
    const untrack = state.track(() => console.log(count))
    
    state.writable.count += 1
     // 1
    
    untrack()
    state.writable.count += 1
     // *nothing happens*

๐Ÿฃ nesting? no problem!

  • we can nest our state to arbitrary depth.
    const state = snapstate({
      group1: {
        group2: {
          data: "hello!",
        },
      },
    })
  • we can track changes to properties, or groups.
    state.track(readable => console.log(readable.group1.group2.hello))
    state.track(readable => console.log(readable.group1))

๐Ÿ‘๏ธ subscribe to any change in the whole state

  • your subscriptions will execute whenever any state is changed.
    state.subscribe(readable => {
      console.log("something has changed")
    })
  • of course you can unsubscribe, too.
    const unsubscribe = state.subscribe(readable => {
      console.log("something has changed")
    })
    
    unsubscribe()

โœ‹ untrack and unsubscribe all

  • stop all tracking
    state.untrackAll()
  • stop all subscribers
    state.unsubscribeAll()

โ›น๏ธ debouncing and waiting

  • the updates that respond to changing state, is debounced.
    this prevents consecutive updates from firing more updates than necessary.
    because of this, you may have to wait before seeing the effects of your update.
    const state = snapstate({count: 0})
    
    let called = false
    state.track(() => {
      called = true
    })
    
    state.writable.count += 1
    console.log(called)
     // false -- what the heck!?
    
    await state.wait()
    console.log(called)
     // true -- oh, okay -- i just had to wait for the debouncer!

โ™ป๏ธ circular-safety

  • you are prevented from writing to state while reacting to it.
  • you can't make circles with track observers:
    state.track(() => {
      state.writable.count += 1
    })
     // SnapstateCircularError -- no way, bucko!
  • you can't make circles with track reactions:
    state.track(
      ({count}) => ({count}),
      () => {
        state.writable.count += 1
      },
    )
    state.writable.count += 1
    await state.wait()
     // SnapstateCircularError -- thwarted again, buddy!
  • and you can't make circles with subscriptions:
    state.subscribe(() => state.writable.count += 1)
    state.writable.count += 1
    await state.wait()
     // SnapstateCircularError -- try again, pal!
  • you can catch these async errors on state.wait().

โœ‚๏ธ substate: carve your state into subsections

  • it's awkward to pass your whole application state to every little part of your app.
  • so you can snip off chunks, to pass along to the components that need it.
    import {snapstate, substate} from "@chasemoskal/snapstate"
    
    const state = snapstate({
      outerCount: 1,
      coolgroup: {
        innerCount: 2,
      }
    })
    
    const coolgroup = substate(state, tree => tree.coolgroup)
    
    // note: coolgroup has no access to "outerCount"
    console.log(coolgroup.readable.innerCount)
     // 2
    
    coolgroup.track(readable => console.log(readable.innerCount))
    coolgroup.writable.innerCount += 1
    await coolgroup.wait()
     // 3
    • a substate's subscribe function only listens to its subsection of the state.
    • a substate's untrackAll function only applies to tracking called on the subsection.
    • a substate's unsubscribeAll function only applies to subscriptions called on the subsection.

๐Ÿ‘จโ€โš–๏ธ strict readonly

  • introducing state.readonly. it's readable's strict and demanding mother-in-law.
  • readonly literally is readable, but with more strict typescript typing.
  • you see, typescript is extremely strict about its readonly properties.
    so much so, that it's very painful to use readonly structures throughout your app.
  • for this reason, snapstate provides state.readable by default, which will throw errors at runtime if you're being naughty and attempting to write properties there -- but the typescript compiler doesn't complain.
  • if your shirt is tucked-in, state.readonly will produce compile-time typescript errors for you.
  • anywhere you find a readable (for example in track and subscribe callbacks), you could set its type to Read<typeof readable> to make typescript strict about it.

๐Ÿ“œ beware of arrays, maps, and other fancy objects

  • snapstate only tracks when properties are written.
  • what this means, is that methods like array.push aren't visible to snapstate:
    const state = snapstate({myArray: []})
    
    // bad -- updates will not respond.
    state.writable.myArray.push("hello")
  • to update an array, we must wholly replace it:
    // good -- updates will respond.
    state.writable.myArray = [...state.writable.myArray, "hello"]
  • this is an entirely survivable state of affairs, but we may eventually do the work to implement special handling for arrays, maps, sets, and other common objects. (contributions welcome!)

๐Ÿงฌ using proxies in your state, if you must

  • snapstate doesn't like proxies in the state, so it makes object copies of them on-sight.
  • this is to prevent circularity issues, since snapstate's readables are proxies.
  • if you'd like to specifically allow a proxy, you can convince snapstate to allow it into the state, by having your proxy return true when symbolToAllowProxyIntoState is accessed.
  • snapstate will check for this symbol whenever it ingests objects into the state.
  • here's an example:
    import {snapstate, symbolToAllowProxyIntoState} from "@chasemoskal/snapstate"
    
    const state = snapstate({
      proxy: new Proxy({}, {
        get(t, property) {
          if (property === symbolToAllowProxyIntoState)
            return true
          else if (property === "hello")
            return "world!"
        },
      })
    })
    
    console.log(state.readable.proxy.hello)
     // "world!"

๐Ÿ’– made with open source love

mit licensed.

please consider contributing by opening issues or pull requests.

  // chase

You might also like...

experimental project for babel-plugin-mutable-react-state

Goalist Mutable React Example This example is an attempt to work with babel-plugin-mutable-react-state on a simpler project to see how complicated wou

Jun 7, 2022

This hook allows you to isolate and manage the state within the component, reducing rendering operations and keeping the source code concise.

React Hook Component State This hook allows you to isolate and manage the state within the component, reducing rendering operations and keeping the so

May 15, 2022

Whoosh - minimalistic React state manager

Whoosh - minimalistic React state manager

Whoosh - minimalistic React state manager Whoosh is a React state manager which entire API consists of exactly one function - createShared(). TL;DR ve

Nov 7, 2022

React Native's Global Alert Component that can be fully customized and without the need of a state.

React Native's Global Alert Component that can be fully customized and without the need of a state.

๐Ÿšฉ React Native Easy Alert React Native Easy Alert Component. Watch on youtube Easy Alert example app. React Native's Global Alert Component that can

Feb 21, 2022

Small (0.5 Kb) react hook for getting media breakpoints state info in runtime

tiny-use-media Small (0.5 Kb) react hook for getting media breakpoints state info in runtime Usage npm i tiny-use-media --save Adn in your react code

Dec 13, 2022

Edvora App is a web application based on an external API, showing data about different types of products and the user can filter these data by choosing a specific state, city or product name. Build with React.js

 Edvora App is a web application based on an external API, showing data about different types of products and the user can filter these data by choosing a specific state, city or product name. Build with React.js

Edvora App is a web application based on an external API, showing data about different types of products and the user can filter these data by choosing a specific state, city or product name. Build with React.js

Mar 11, 2022

๐Ÿ–ฑor โŒจ๏ธ? ๐Ÿคทโ€โ™€๏ธ, but hopefully use-hover-state works on the "user intent" not the "device"

useHoverState() The one aware of keyboard navigation as well ๐Ÿ˜‰ npm i use-hover-state A React hook for tracking user interaction with the DOM elements

Aug 11, 2022

a babel plugin that can transform generator function to state machine, which is a ported version of typescript generator transform

Babel Plugin Lite Regenerator intro This babel plugin is a ported version of TypeScript generator transform. It can transform async and generator func

Jul 8, 2022

React Query wrapper for NextAuth.js session management

React Query wrapper for NextAuth.js session management

NextAuth.js React-Query Client @next-auth/react-query React Query wrapper for NextAuth.js session management. Overview This is an alternative client f

Dec 16, 2022
Comments
  • rewrite: recursive observable properties

    rewrite: recursive observable properties

    i thought i could use composeSnapstate to get away without figuring out recursive observables.

    it won't suffice.

    a more sophisticated approach is necessary, because we need to be able to track changes to the objects which contain observable properties.

    it seems like this may be a doozy.

    enhancement 
    opened by chase-moskal 1
  • ๐Ÿ“œ tracking picks up new conditional observables to watch

    ๐Ÿ“œ tracking picks up new conditional observables to watch

    currently, i think, if you create a snap.track, it will record the observables that you access, and watch those for changes to trigger your callback.

    but if your observer accesses things conditionally, they may not be picked up and watched.

    we should figure out if we can implement trackers in a way that pick up these conditional observables.

    enhancement 
    opened by chase-moskal 0
  • ๐Ÿงžโ€โ™‚๏ธ rug-pull resistance

    ๐Ÿงžโ€โ™‚๏ธ rug-pull resistance

    in branch rugpull2, a failing test has been added, demonstrating that rug-pulling is a problem.

    • rug-pulling is a phenomenon when you copy an object reference from the state, save it for later, and then change that object in the state.
    • users expect that rewriting the object in the state won't affect the properties in the old reference they saved. it does.
    • this is because they don't really hold a reference to the real object โ€” they hold a reference to a snapstate proxy.
    • that proxy will always query the state tree to fetch properties.

    this is a doozy.

    • for now, there's a workaround โ€” users can call unproxy on their object reference, which will eliminate this problem. it makes a new copy of whatever object they want to hold onto for later.

    we need this behavior to not surprise users. i suspect a possible solution could be:

    • snapstate could keep weak references to the entire history of state.
    • when a user grabs an object from the state, they are actually grabbing a proxy for that particular point in state history.
    • however, this strategy potentially has dire consequences:
      • substates must be able to refer to current state, not historical state.
      • maybe we could introduce something like currentState, just for substates to use?
    bug enhancement 
    opened by chase-moskal 0
Owner
Chase Moskal
developer on an open source rampage
Chase Moskal
Tiny and powerful state management library.

BitAboutState Tiny and powerful React state management library. 100% Idiomatic React. Install npm install --save @bit-about/state Features 100% Idioma

null 53 Nov 5, 2022
Create a performant distributed context state by synergyzing atomar context pieces and composing reusable state logic.

Synergies Create a performant distributed context state by synergyzing atomar context pieces and composing reusable state logic. synergies is a tiny (

Lukas Bach 8 Nov 8, 2022
Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.

Recoil ยท Recoil is an experimental set of utilities for state management with React. Please see the website: https://recoiljs.org Installation The Rec

Facebook Experimental 18.2k Jan 8, 2023
๐Ÿป Bear necessities for state management in React

A small, fast and scaleable bearbones state-management solution. Has a comfy api based on hooks, isn't boilerplatey or opinionated, but still just eno

Poimandres 25.5k Jan 9, 2023
๐Ÿ High performance subscription-based form state management for React

You build great forms, but do you know HOW users use your forms? Find out with Form Nerd! Professional analytics from the creator of React Final Form.

Final Form 7.2k Jan 7, 2023
A state management library for React, heavily inspired by vuex

Vuex - But for React! โš› If you know vuex, you know it's as close as we get to a perfect state management library. What if we could do this in the reac

Dana Janoskova 103 Sep 8, 2022
Twitter-Clone-Nextjs - Twitter Clone Built With React JS, Next JS, Recoil for State Management and Firebase as Backend

Twitter Clone This is a Next.js project bootstrapped with create-next-app. Getting Started First, run the development server: npm run dev # or yarn de

Basudev 0 Feb 7, 2022
Learning how to use redux - a state management library

Redux Learning how to use redux - a state management library What is Redux? Redux is a state management library for JS apps. It centralizes applicatio

Melvin Ng 3 Jul 18, 2022
A Higher Order Component using react-redux to keep form state in a Redux store

redux-form You build great forms, but do you know HOW users use your forms? Find out with Form Nerd! Professional analytics from the creator of Redux

Redux Form 12.6k Jan 3, 2023
Real state property listing app using next.js , chakra.ui, SCSS

This is a Next.js project bootstrapped with create-next-app. Getting Started First, run the development server: npm run dev # or yarn dev Open http://

null 1 Dec 19, 2021