overstated v2.2.2
Overstated
React state management library that's delightful to use, without sacrificing performance or scalability.
Features
- Easy: there's barely anything you need to learn before you can start using Overstated, you just have to wrap your app with
Provider, define some initial state and some methods for updating it in aStoreand then access the state and methods you need in your components either via theuseStorehook orconnect. - Performant: Overstated has been designed from the ground up with performance in mind, everything you need to write very performant applications (selector function,
autosuspend,debug) is built-in. - Scalable: you can write big apps with this. You can
connectto multiple stores with a single call,composemultiple stores easily and usemiddlewares. - TypeScript-ready: Overstated has been written in TypeScript and enables you to get a fully typed app with minimal effort.
Check out the FAQ section for some commonly asked questions.
Install
npm install --save overstatedUsage
Provider
The Provider component needs to wrap your app in order for this to work:
import * as React from 'react';
import {render} from 'react-dom';
import App from './app';
import {Provider} from 'overstated';
render (
<Provider>
<App />
</Provider>,
document.getElementById ( 'app' )
);- ℹ️ An error will be thrown if you forget to do this.
- ℹ️ You can inject some stores that will be used instead of the regular ones, even if you are pre-instantiating them, via the
injectattribute:<Provider inject={[StoreInstanceFoo, StoreInstanceBar]}>, this is useful for testing purposes.
Store
The Store is where you define your initial state and the methods for updating it. Then your components will retrieve the state and methods they need via useStore or connect and they will be re-rendered when the state changes.
Core methods
This is the core interface of a Store:
class Store<State> {
state: State = {};
setState ( updater: State | Function, callback?: Function ): Promise<void>;
}It is modeled after a React class component and it works in a similar way:
- There's a
stateobject, which:- Must be a plain object, other kinds of data (primitives, arrays, functions etc.) aren't supported.
- You must never mutate directly.
- There's a
setStatemethod, which:- Can accept either a new state object (which will be merged with the current one) or a function, which will be called with the current state as its first argument, which should return a new state object.
- Can accept a callback function, which will be called once the state has been updated.
- Returns a promise, which you can
awaitif you need to wait until the state has been updated.
You define your stores by extending the default one:
import {Store} from 'overstated';
class CounterStore extends Store<{value: number}> {
state = {
value: 0
}
increment = () => {
return this.setState ( state => ({ value: state.value + 1 }) );
}
decrement = () => {
return this.setState ( state => ({ value: state.value + 1 }) );
}
}- ℹ️ If you're using TypeScript you should provide the type of the state object.
- ℹ️ Never mutate the state directly (
this.state.foo = 123), always usesetState(this.setState ({ foo: 123 })). - ℹ️ Don't read state immediately after setting it, as it won't be updated immediately.
- ℹ️ If the next state depends on the current state you should call
setStatewith a function, to make sure that the state is computed properly and you aren't using an older version of the state object. - ℹ️ You should always define your methods like this:
foo = () => {}rather than like this:foo () {}so that you will never have to callFunction#bindon them when using them. - ℹ️ If your method is updating the state you should always
returnthe value returned bysetState, so that other methods canawaitit if needed. - ℹ️ In general stores are just plain classes, so testing them is easy.
Suspension
Overstated's stores have the ability to "suspend" React re-renders that would otherwise occur when their state changes, this is useful when you want to update the state many times but only want to cause one re-render. This is the related interface:
class Store {
suspend ( options?: SuspensionOptions ): void
unsuspend ( options?: SuspensionOptions, callback?: Function ): Promise<void>
isSuspended (): boolean
}The supported options object has the following interface:
{
propagateUp: false, // Whether to propagate up the suspension to parents
propagateDown: false // Whether to propagate down the suspension to children
}- The
suspendmethod allows you to suspend re-renders.- Can accept a options object, useful for propagating the suspension up to parent stores and/or down to child stores, read more about them in the
composesection.
- Can accept a options object, useful for propagating the suspension up to parent stores and/or down to child stores, read more about them in the
- The
unsuspendmethod allows you to unsuspend re-renders: if before callingunsuspenda re-render has been prevented it will now be triggered.- Can accept a options object, useful for propagating the suspension up to parent stores and/or down to child stores, read more about them in the
composesection. - Can accept a callback function, which will be called after any potential re-renders caused by this function.
- Can accept a options object, useful for propagating the suspension up to parent stores and/or down to child stores, read more about them in the
The
isSuspendedmethod allows you to check if re-renders are currenly suspended.ℹ️ If for instance you call
suspend3 times you should also callunsuspend3 times to resume operating as normal.- ℹ️ Using this APIs directly can be cumbersome, as you'd probably want to use it in all of your methods that update the state, and you'd have to make sure that
unsuspendgets properly called even if your methods throw an error, otherwise re-renders will remain suspended forever. Instead you should useautosuspend, which takes care of everything for you.
autosuspend
autosuspend is a method for automatically and safely suspend-ing React re-renders when a method gets called and unsuspending them right before returning from it. This is useful for ensuring that even if your methods update the state many times they only cause 1 re-render. You might not want this behavior, so it's opt-in:
import CounterStore from './counter_store';
CounterStore.autosuspend ();The autosuspend method accepts an option object with the following shape:
const options = {
children: true, // Whether to autosuspend children too
methods: /^(?!_|(?:(?:get|has|is)(?![a-z0-9])))/, // Methods matching this regex will be autosuspended
methodsInclude: undefined, // Methods matching this regex will be autosuspended, has higher priority over the "methods" regex
methodsExclude: undefined, // Methods matching this regex will be autosuspended, has higher priority over the "methods" regex and the "methodsInclude" regex
propagateUp: false, // Whether to propagate up the suspension to parents
propagateDown: false // Whether to propagate down the suspension to children
};methodsis a regex, if your method's name matches that regex then it will be autosuspended. By default methods starting with_,get,hasoriswon't be autosuspended, as we are assuming that those won't be updating the state.methodsIncludeis a regex, if your method's name matches that regex then it will be autosuspended, has higher proprity overmethods.methodsExcludeis a regex, if your method's name matches that regex then it will not be autosuspended, has higher proprity overmethodsandmethodsInclude.propagateUpchecks whether parent stores (read more about them in thecomposesection) should get suspended too, generally you won't need to set this option manually.propagateDownchecks whether children stores (read more about them in thecomposesection) should get suspended too, generally you won't need to set this option manually.childrenchecks whether children stores (read more about them in thecomposesection) should get autosuspended too, generally you won't need to set this option manually.ℹ️ If you try to autosuspend a store twice an error will be thrown.
- ℹ️ When autosuspending a store its children stores (if any) will be autosuspended too, read more about them in the
composesection. - ℹ️ You can also set those options in each store's class as the value of their
autosuspendOptionsinstance property. - ℹ️ It's recommended to include or exclude new methods via the
methodsIncludeandmethodsExcludeoptional regexes, so that you won't have to manually modify themethodsregex. - ℹ️ Since this relies on the individual methods' names you'll have to make sure that your minifier isn't minifing function names.
middlewares
You can register and unregister middlewares on each store, they will be called every time the store gets updated, including if it was updated from within a middleware, and they will receive the previous state object as their first argument. This is the related interface:
class Store {
middlewares: Function[] = []; // Array of currently registered middlewares
registerMiddleware ( middleware: Function ): void;
unregisterMiddleware ( middleware: Function ): void;
}middlewarescontains all the currently registered middlewares.registerMiddlewareallows you to add a new middleware.unregisterMiddlewareallows you to remove an existing middleware.
You can use middlewares like so:
import {Store} from 'overstated';
class MyStore extends Store {
constructor () {
super ();
this.registerMiddleware ( this.middlewareLog );
}
middlewareLog = ( prevState ) => {
console.log ( 'prevState:', prevState );
console.log ( 'state:', this.state );
}
}- ℹ️ You should use as few middlewares as possible, as they might get called very often.
- ℹ️ Updating the state from within a middleware also causes all middlewares affected by that to be called again, so you should avoid doing so if possible.
- ℹ️ Registering middlewares in parent stores (read more about them in the
composesection) is discouraged, as they will also get called if a children store's state is updated. - ℹ️ It's good practice to name your middlewares
middleware*, as that makes it clear that those are middlewares.
useStore
useStore is a React hook for accessing a store's state and methods from within a component. You should always use useStore, unless you can't use React hooks, instead of connect, because it preserves your TypeScript types completely. This is its interface:
useStore ( storeClassOrInstance, selector: store => R, deps: ReadonlyArray<any> = [] ) R- The first argument can be either a store class or a pre-instantiated store.
- The second argument is a selector (a function) which will receive the instance of the passed store and will return something, whatever will be returned by the selector will also be the return value of the
useStorecall. - The third argument is an optional array of dependencies. If your selector relies on external dependencies you must include all of them in the
depsarray, like you would when usinguseCallback. By default the array of dependencies will be an empty array, so the first selector function will get reused indefinitely.
You can use it like so:
import CounterStore from './counter_store';
import {useStore} from 'overstated';
function Counter () {
const {value, increment, decrement} = useStore ( CounterStore, store => ({
value: store.state.value,
increment: store.increment,
decrement: store.decrement
}));
return (
<div>
<div>{value}</div>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}- ℹ️ Every time the store's state changes Overstated will re-run the selector passed to
useStoreand compare the previously returned value with the current one, if nothing changed we stop there, if something changed the component gets re-rendered. - ℹ️ If you pass
useStorea pre-instantiated store then you won't need to retrieve its methods this way, but instead you can access them directly (CounterStore.increment), this has a couple of performance advantages:- The object returned by the selector will be slightly faster to compare against the previous one.
- If in a component you don't need to access any state at all from a selector, but only need to access its methods, then you can entirely avoid using
useStorefor that component.
- ℹ️ If you need to access state/methods from multiple stores just call
useStoremultiple times. - ℹ️ When using
composeyou may encounter the scenario where you need to access some methods from storeA, but those methods actually access the state of storeB, so passing storeAtouseStorewon't properly update the component because the changes in state will happen in storeB. There are 2 ways to solve this issue:- Pass
composea store higher-up in the hierarchy that has both storeAand storeBbelow itself. - Pre-instantiate your stores, pass store
BtouseStore, but then reference the pre-instantiated instance of storeAinside your selector function. This solution, albeit slightly more performant, is discouraged because you'll have to reason about what stores' states your selector is actually accessing, and that's very error prone.
- Pass
connect
connect is an alternative to useStore. You should generally prefer useStore over this as connect doesn't fully preserve your TypeScript types (yet). This is its interface:
// USAGE 1
connect ( storeClassOrInstance )( ReactComponent );
// USAGE 2
connect ({
store?: storeClassOrInstance,
stores?: storeClassOrInstance,
selector?: Function
})( ReactComponent );
// USAGE 3
connect ({
store?: storeClassOrInstance,
stores?: storeClassOrInstance,
selector?: Function,
render?: ReactComponent
});It is a decorator, so you can also call it like @connect ({ ... }) before a React's class component definition.
It can be used in either of those 3 ways:
- Pass it a store class or instance, and then pass it a React component.
- Pass it some options, and then pass it a React component.
- Pass it some options which include the
renderoption, that will be used as the React component.
The options object supports the following options:
store: a store class on instance.stores: an array of store classes or instances.selector: a selector which, similarly to howuseStore's selector works, will be called with the following object:{ store, stores }and will have to return the props to pass to the component.render: a component to render. This property exists so that if you wish to you can define your React function component there and have everything neatly contained within a single object and cleanly indented.
compose
compose allows you to set a parent <-> child relationship between your stores, so that a parent store has access to its children and the children have access to their parent (and as a consequence to each other too). This is useful because as your app grows you might want to split your app's state and methods into multiple stores that can talk to each other.
compose ({
child1: StoreClassOrInstance1,
child2: StoreClassOrInstance2,
// ...
})( ParentStore )You can use it like so:
import {compose, Store} from 'overstated';
class Foo extends Store<{value: number}, App> {
state: {
value: 123
}
setValue = ( value: number ) => {
return this.setState ({ value });
}
example = () => {
this.ctx.updateBar (); // Accessing the parent
this.ctx.bar.setValue ( 'Hi' ); // Accessing a sibling via the parent
}
}
class Bar extends Store<{value: string}, App> {
state: {
value: 'hello'
}
setValue = ( value: string ) => {
return this.setState ({ value });
}
example = () => {
this.ctx.updateFoo (); // Accessing the parent
this.ctx.foo.setValue ( 0 ); // Accessing a sibling via the parent
}
}
@compose ({
foo: Foo,
bar: Bar
})
class App extends Store<{}, undefined, {foo: Foo, bar: Bar}> {
updateFoo = () => {
return this.foo.setValue ( 10 ); // Accessing a sibling
}
updateBar = () => {
return this.bar.setValue ( 'hello there!' ); // Accessing a sibling
}
}It works like this:
- Firt of all if you're using TypeScript the Store class accepts a couple more types: the first type, which we already encountered, describes the interface of the state, the second type describes the inferface of the parent store (if any) and the third stype describes the interface of the children stores (if any).
composewill make sure that all stores classes/instances passed to it are accessible from the parent, so that if you tellcomposeto map theFoostore to thefooproperty then you can access it viathis.foofrom the parent store.composewill make sure that all child stores can access the parent (and as a consequence each other too) viathis.ctx.- If the stores passed to
composearen't already instantiated they will be instantiated right when the parent store itself is instantiated. - Finally whenever a child store's state gets updated the parent's state will also be updated: the content of the state will be the same but the state itself will be a different object, this is done in order to ensure that if you connect to a parent store via
useStoreorconnectyour component will re-render properly also when the children stores get updated. For the same reason yourmiddlewaresregistered in a parent store will also get called when the children's stores get updated.
- ℹ️ It's important to note that if you pass a parent store to
useStoreorconnectthen your component could potentially attempt to re-render even when state from other stores which it doesn't care about gets updated. To avoid this potentially significant performance penalty you should instead, whenever possible, pre-instantiate your parent store and then pass the child store you need directly (MyParentStore.foorather thanMyParentStore) touseStoreorconnect.
debug
debug is a simple way to access your stores and their state from the the devtools and see at a glance when any store's state changes.

This is its interface:
debug ({
collapsed: false, // Whether the logged group should be collapsed
logNewStores: false, // Whether to log when a new store is instantiated
logStateDiffChanges: true, // Whether to log diffs (added, updated, removed) state changes
logStateFullChanges: true // Whether to log the previous and current state in their entirity
});You can use it like so:
import {debug} from 'overstated';
debug ();Once called debug also defines a global object named OVERSTATED, which you can access from the devtools. This is its interface:
window.OVERSTATED = {
stores: { ... }, // Access all stores
states: { ... }, // Access all states
log (): void // Log all states
};You can use it like so:
OVERSTATED.stores.CounterStore.increment (); // If you want to call a method manually
OVERSTATED.states.CounterStore; // If you want to access a store's state quickly
OVERSTATED.log (); // If you want to see all your stores' states at onceℹ️ It's important to call
debugbefore rendering the app and before manually instantiating any stores.ℹ️ Make sure to enabled
debugonly during development.
Hooks
Hooks provides a simple way to "hook" into Overstated's internal events. Each hook has the following interface:
class Hook {
subscribe ( callback: Function ): void
unsubscribe ( callback: Function ): void
}subscriberegisters a function for being called every time that hook is triggered.unsubscribeunregisters a previously registered function.
These are all the hooks currently available:
const Hooks = {
store: {
new: Hook // This hook is actually used to implement `debug`
}
};You can use hooks like so:
import {Hooks} from 'overstated';
Hooks.store.new.subscribe ( store => console.log ( 'New store just instantiated:', store ) );If you need some other hooks for your Overstated plugin let me know and I'll make sure to add it.
We currently don't have an official "Overstated DevTools Extension", but it would be super cool to have one. I'm thinking basically one could implement everything implemented by
debugbut in a panel accessible from the browser's devtools. Perhaps some other cool features could be implemented like time travel debugging, for restoring the app to a previous state in time. If somebody would like to implement this please do let me know! 😃
FAQ
Why not using Unstated?
As you might have noticed Overstated is modeled after Unstated, which I've been using in the past few months and mostly liking. But Unstated has a few problems:
- Subscribing to a store is cumbersone, our
useStoreis so much nicer, especially if you're using TypeScript. - It's impossible to centralize stores like we can do with
compose, so except for the smallest apps you have to subscribe to multiple stores or deal with a massive single-file store. - There's no concept of
suspension, so if you update the state within a loop enough times the app could basically crash because too many re-renders are triggered. - There's no way to define
middlewares, and sometimes you want them. - There's no
debug-like capabilities built-in. - There's no concept of a "selector" function, so your components always re-render even if they don't care about the particular bit of state that changed.
Some of these problems can be solved via third-party libraries, which me and others have written and are available here, but some things just can't be implemented in a performant manner when you're building on top of Unstated, for instance I ended up adding 5+ Higher-order-Components to each of my components that needed to subscribe to a store. And by implementing all this in the core you can do some nice things like skipping re-renders much earlier in the process.
In the end I decided that basically rewriting Unstated, but with performance in mind, more features and better TypeScript types, was the right option for me.
I don't think there's any reason to use Unstated over Overstated frankly. You can't even open an issue in Unstated's repository because the issues page is disabled.
Why not using Redux?
I'm not as familiar with Redux as I am with Unstated, but I think some reasons for using Overstated over Redux might be:
- If you don't know or like Redux already I think Overstated will be much easier to learn and use as we don't have "actions", "reducers", there's no big switch statement to write, nothing to "dispatch", no "payloads", nothing weird that needs to be done for asynchronous actions, no need to write TypeScript types for actions, we just call some methods and pass them arguments, just like you would normally do.
- I'm not sure how Redux handles actions dispatched from within actions, via
suspensionandautosuspendwe can make sure that that causes a single re-render of your app, maybe you can do the same with Redux too? Maybe you're not supposed to do that? I don't know.
But Redux is definitely a fine library, which has been battle-tested and which has an amazing ecosystem around it.
I guess ultimatelly the reason to pick one over the other might be a matter of personal preference.
How to write a very performant app?
In order to write a very performant app there's only one golden rule:
- Make sure that your components are only re-rendered when necessary, and make sure that each render is fast.
To break that down into smaller to digest tips, roughly ordered by importance:
- Mesure, measure and measure. Make you sure you know what the slowest parts of your apps actually are before attempting any optimizations.
- Always use a selector function.
- Make sure your selectors are fast.
- Use
autosuspendto make sure that 1 method call causes 1 re-render. You may not always want this, but in general I think it's a good starting point. - Write smaller stores that contain less state, so that they each of them will trigger fewer re-renders.
- If you're using
compose, pre-instantiate your parent store and pass the child store directly touseStoreorconnect, so that your component won't attempt to re-render if other stores' states change. - Use as few middlewares as possible, there's probably another way to implement the same thing which doesn't involves middlewares at all.
- Make sure your middlewares are fast.
- Pre-instantiate your stores and use their methods directly rather than retrieving them via
useStoreorconnect. - Always define your methods like this:
foo = () => {}rather than like this:foo () {}so that you will never have to callFunction#bindon them when using them.
And in general make sure to also follow JavaScript/React's best practices with regards to performance.
Thanks
- @webstrand: for providing the trickiest TypeScript annotations we needed.
- @jamiebuilds: for writing Unstated, the first state management library that really resonated with me.
- The @facebook/react team: for developing React and hooks, which are wonderful.
- My mom too, since this thanks section is getting akwardly long anyway.
License
MIT © Fabio Spampinato
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
