State Mint 🌿
A state layer that keeps your React project fresh 🌿 Designed for React developers, State Mint is a boilerplate-free state management and persistence solution with spectacular performance and the best-available developer experience. Click here to read about the process of creating State Mint, and about its future direction (feel free to PR).
view minimal example implementation
import React from 'react'import mint from 'state-mint'import render from 'react-dom' // define the store as an ES6 class: state = showingModal: false this // 'mint' your store(s) // define and 'mint' a component that uses the 'modal' store...// because the component references the 'modal' store, changes to// modal store state will trigger a rerenderconst Modal =
Highlights
-
🤯 simplicity • use all features without visibly touching more than a single, one-parameter function from this library
-
🧛♂️ persistence • highly configurable data persistence with session storage, local storage and/or cookies on web, and async storage and/or secure store on React Native
-
👂 subscription inference • components are intelligently subscribed to listen for changes in the stores they reference (or, you can specify subscriptions)
-
🎯 store-to-store communcation • stores can directly access oneanother's data
-
🎩 timing isn't everything • store instances can be initialized and connected to components asynchronously • new stores will (with zero extra configuration) collect subscriptions from previously-initialized components that reference the new store
-
😷 keep your state safe from direct mutation with a re-implemented, data-persisting setState, which can be used identically to React's Component.setState
-
🎣 add lifecycle hooks to functional components with no additional HOC
other things that're good to have...
-
👩👧👦 no dependencies
-
📦 under 3kbs gzipped
-
🍻 plays nice with older versions React
-
🔐 (give me until October 4th) Flow & TypeScript typings
Guide
Installation
available through the NPM registry
yarn add state-mint
build formats
ES Module
CommonJS
const mint = default
UMD
var mint = windowStateMintdefault
Quick Start
import React from 'react'import mint from 'state-mint'import render from 'react-dom' // define your store class// setState will trigger rerenders when & where appropriate state = count: 0 this this // 'mint' takes in an object with keyed store classes... // once the store has been initialized ^, use mint again to wrap a component that uses the counter storeconst App =
To use the counter store above in other modules, import 'mint' again and wrap the component that needs access:
some-other-file.js
import mint from 'state-mint' const AnotherCounterComponent = // ^ that's it
Why?
the ideal
State management and persistence shouldn't require the learning of new conventions; React developers are familiar with HOCs (higher-order components) and Component.setState. State management libraries should allow implementations to involve as little library code as possible, and allow users to focus on defining their data and actions, without excessive boilerplate or 3rd-party plugins and middleware.
the reality
Opinionated state management often simplifies debugging and collaboration. Once a project reaches ~5,000 lines, chances are that you're thinking about how to enforce rules for consistency and maintainability. However, you might want to tackle this problem in a way more suited to your project needs and coding style. A lot of the time, this will differ from de facto approaches.
the history
Before React came onto the scene, global state management was, for many projects, somewhat of an afterthought. For simple websites, one might hastily throw global state into the window object. Nowadays, for the sake of enabling smoother application evolution, state management needs to eliminate the possibility of overwrites––usually through careful scoping or synthetic immutability ("synthetic" because JavaScript is not a functional programming language). Tools that take a functional approach to state management can simplify otherwise complex data pipelines, and make it possible to use back-tracking middleware (not to mention keep you safe from stack trace hell). However, the look of existing implementations is horrific.
the horror
Redux : Conventional Redux requires that you separate action types from their logic, logic from its data, and data from its triggering of subsequent actions. Depending on how you like to work, this decoupling is either the best or the worst approach (if you lay in the middle, chances are you might not need Redux).
MobX: comes in a few different flavors. Classic MobX is a step in a more intuitive and object-oriented direction. On the downside, it forces users to specify which store members are observable, which is a new convention (in React, by default, a Component's state member is observable, and using setState will trigger a rerender that uses the new state data). This being said, the new convention does lead to a performance gain by haulting unnecessary rerenders in your DOM tree. MobX encourages explicit mutation, which is a React anti-pattern. Meanwhile, ...
MobX State Tree could be described as having the best of both mutability and immutability (reactive variable assignment, back-tracking & snapshot debugging). It let's you nest store data in a way that scales, and your models always stay in sync... but its implimentation is opinionated, cluttered and unattractive.
Apollo Link State: If your app interfaces with a GraphQL server, this could be a good solution. The Apollo ecosystem is vibrant and rapidly evolving. Apollo Link State gives users a strong API for syncing fetched data in memory and offline persistence. However, it's also very opinionated, and unless you're already using Apollo Client, it probably isn't the best solution.
Unstated: as far as alternatives go, Unstated is the least opinionated with the lowest learning curve. The underlaying mechanism is pretty cool: behind the scenes, stores are initialized inside a Consumer, which then passes the store data to its parent provider, which then passes the data to all store Consumers. This pattern is cool, but a little hackey, and results in extra operations with each update. It also means you need to use the Store contructor as a key to the instance, (no support for multiple instances of the same Store). Another disadvantage is that data can only be accessed within a render method (aka. no store usage in lifecycle methods) without a user-defined HOC. Plus, using the ContextAPI means that any operation that updates the state of any store will trigger a re-render of all mounted "connected" components.
overview
-
Define your stores as ES6 classes and use
state
andsetState
just as you would when extending React.Component. For all intensive purposes, there's no difference in their usage. -
Import the
mint
function from thestate-mint
package, and call it with an object that has your Store class constructors as the values (key them however you'd like to later reference the instance). -
Use the
mint
function again to wrap your components, thereby subscribing them to the stores they reference.
... feel free to switch up steps 2 and 3
Minting
the thinking behind the name
The name was selected as it relates to the idea of an industrial facility that manufactures coins
"Mint: a place where money is coined, especially under state authority" (dictionary.com)
While it is a nice play on words (especially relating to React.setState), the term "mint" also suggests something to the effect of governing (digital) assets. All-in-all, I believe it's a good fit for this library, and could be adopted by others as a term for describing the instanciation of data stores.
minting stores
Stores are defined as ES6 classes, and then "minted" with the default-exported function of the state-mint
package. By passing your stores to that mint
function, the stores get extended with the setState method, along with performance enhancements, persistence features, and more. The extended class is then instanciated and placed appropriately within a scope that can only be accessed by minting a component. This prevents accidental overwrites and other conflicts.
state = whosStore: 'mine' // this mints one instance,// with a key of 'my'
minting components
You can connect any component to store(s) data by simply wrapping the component in the same mint
function as before. In minting a component, State Mint will inspect which stores the component (including lifecycle methods) reference. This feature is called 'subscription inference.' When a given store's state is updated, its subscribed components are rerendered.
const Whos =
switching up the order
Subscription inference will work, even if you define a store after instanciating a component that uses the store's data. A simple if (props.$.storeName)
in your component will safeguard against errors that come about from the store being undefined. Once the store is defined, the component will be subscribed to it, and will rerender with the store data. In other words, you don't need to mint any stores in order to mint a component.
const Whos = = state = whosStore: 'mine'
subscriptions
There are two ways to subscribe (connect) components with State Mint. In either case, subscribing a component to a store will do two things: (1) it will make the store's data accessible through props with a key of $
(props.$
) and (2) it will rerender the component upon any changes to the state of stores to which the component is subscribed.
manual assignment
The first way to subscribe a component to a store is to mint the store with a second argument, an array of store keys. For instance:
const Header =
subscription inference
In the vast majority of use cases, subscription inference will work equally-well. The underlaying operations involve converting your component and any lifecycle methods or hooks to strings, and and then parsing out whether a given store is referenced. Don't worry about the effects of destructuring or other syntactical abstractions of the reference.
const Header =
managing an instance's subscription
Let's say you want to subscribe or unsubscribe a component that's already been instanciated:
const Countdown =
Persistence
Default settings
By default, persistence is disabled. To enable persistence without configuration, simply set an instance variable persist
to true
. Every time the state changes, it will be persisted with localStorage (set strategy in React Native).
export default class Counter { state = { count: 0 } + persistence = true increment = () => this.setState({ count: this.state.count + 1, }) decrement = () => this.setState({ count: this.state.count - 1, }) }
Custom strategy
To use another persist strategy, set persist to an object containing a strategy
prop. Assign strategy
to the strategy you wish to use (current options: localStorage, sessionStorage, document.cookie, AsyncStorage, and SecureStore).
export default class Counter { state = { count: 0 } + persistence = { strategy: window.sessionStorage } increment = () => { this.setState((lastState) => ({ count: lastState.count + 1, })) } decrement = () => { this.setState((lastState) => ({ count: lastState.count - 1, })) } }
React Native
To use AsyncStorage or SecureStore (React Native only), you'll need to first import the storage provider:
import { AsyncStorage } from 'react-native' export default class Counter { state = { count: 0 } + persistence = { strategy: AsyncStorage } increment = () => { this.setState((lastState) => ({ count: lastState.count + 1, })) } decrement = () => { this.setState((lastState) => ({ count: lastState.count - 1, })) } }
Specify what data to persist
We don't always want to persist the entire state, and sometimes we want to persist data outside of state (instance variables). Be careful though, persisting functions will result in an error (as they cannot be converted to JSON).
In this particular case, we want to persist info about the user, but we don't want to persist whether or not to show the bio.
const DEFAULT_STATE = { loggedIn: false, username: null, bio: null, bioShowing: false,} export default class Account { state = { ...DEFAULT_STATE } persistence = { strategy: window.localStorage, // return an object containing the data you wish to persist+ fromStore: () => {+ const { bioShowing, ...user } = this.state+ return user+ }, // when persisted data gets retrieved, place it where it goes+ toStore: (persistedData) => {+ this.setState((lastState) => ({+ ...lastState,+ ...persistedData,+ }))+ }, } logIn = () => { this.setState((lastState) => ({ ...lastState, loggedIn: true, username: 'harrysolovay', bio: 'I really like State Mint!', })) } toggleBioShowing = () => { this.setState((lastState) => ({ ...lastState, bioShowing: !lastState.bioShowing, })) } logOut = () => { this.setState(DEFAULT_STATE) } }
manually trigger a persistent save
Often times, you'll want to persist your data independent of state, or application memory for that matter. By defining fromStore & toStore, you can establish the flow of data in and out of persistent storage. From this flow, State Mint checks to see if persistent storage references state at all. If it does, then calling setState will trigger a persistent save. Otherwise, setState will leave persistent storage untouched. Aka., you can stop using setState if the only class features you're using are instance variables and State Mint's persistence; manually trigger a persistent save by calling this.persist
with no arguments from within your store class. It won't trigger a re-render, but it will save the data to your chosen or the default strategy.
export default class SomeToggle { outOfStateBoolean = false persist = { strategy: window.localStorage, // return an object containing the data you wish to persist fromStore: () => { const { outOfStateBoolean } = this return outOfStateBoolean }, // when persisted data gets retrieved, place it where it goes toStore: (persistedData) => { this.outOfStateBoolean = persistedData }, } toggle = () => { this.outOfStateBoolean = !this.outOfStateBoolean+ this.persist() } }
Lifecycle hooks
State Mint makes use of the higher-order component (HOC) pattern for which the React team advocates.
architectural sidenote
For the time being, using HOCs seems to be the safest way to compose user-defined components that access a state-dependent assortment of stores and settings. However, in my experimentation, I did find another pattern which performs better for the wrapping of stateful components: the wrapping function could extend a new class with the user-defined component (which extends React.Component). Inside of this newly-generated class, ES6 symbols would be used to mask private properties of the wrapper. This way, there's no overriding of props.Because of the use of stateful HOCs, there's little reason to define your wrapped component as also stateful. Instead, boost performance by using using minted stores (instead of local state) and adding lifecycle hooks as a static property of your (newly) functional component:
import mint from 'state-mint' const Counter = $: counter <div> <button ='-' = /> <span = /> <button ='+' = /> </div> Counter { console } { console } Counter
Now, when you use the Counter component, the constructor and componentDidMount hooks will be triggered from the stateful component in which it is contained.
FAQ
Q) How can I use setState
in my store without extending another class where setState
is defined?
A) Before mint
constructors your store, the setState
method is attached to the class prototype (precompiled, it defines a new class that extends yours). Although your class doesn't have a setState method upon its initial definition, it will upon runtime.
Q) Why doesn't it use React's Context API?
A) While React@^16.3 Context is polyfilled for older versions or react, I wanted State Mint to work without the version or peer dependency. Plus, using React Context would be overkill. Context Providers rerender all children upon any state change (no faster paints). By saving a given store's subscriber components' references, the door is open to more customization of behavior.
LICENSE
MIT