/!\ Important : This doc is relevant for up to version 1.4.6. New doc coming soon.
react-stored
The ultimate useStore
implementation. The power and simplicity of useState
, but with persistence, global key-based synchronization without context, speed and reference optimization, safety checks, and other cool stuff.
Ever dreamed of such of feature but couldn't come up with a 100% satisfying solution ? Well, this package is for you.
It is designed to :
- provide you with a reliable key-based
useState
-like feature calleduseStore
, - with auto-sync with other
useStore
calls sharing the same key, - without context,
- with persistence across page reloads and browser sessions (or not),
- with configurable safety asserts on deserialization and default fallbacks,
- with no unnecessary rerender, ever,
- with SSR capacities (see FAQ),
- with simplicity and cool local / global configuration options,
- with zero dependency (other than React of course),
- with very little extra bundle size (+ 2.6 KB (4 times less than this very readme))
Install
yarn add react-stored
Quick demo
Say you have the two following components far away from one another in the tree :
;; { const counter setCounter = ; return <> <h1>Counter : counter</h1> <button onClick= >Increment</button> </> ;}
;; { const counter setCounter = ; return <> <h1>Counter : counter</h1> <button onClick= >Reset</button> </> ;}
Since they share the same key ('counter'
), they actually seemlessly share the same value and keep one another in sync no matter how hard you try to unsync them. Even better : if you refresh the page, nothing changes. The values are persistent. You better like them, because they ain't going anywhere unless you change the key or... the key prefix (see config).
The second argument to useStore
, the number 0
in this case, represents the default counter value, as no persistent save can be found the first time around.
Reference
useStore
hook
1- The This is the cornerstone of this package. It 'connects' you to a specific store, identified by key, and returns the value at that location as well as an update function. Its overall feel mimics useState
. It also listens to any outside change, and rerenders accordingly to keep all parts of your UI in sync.
It can take up to 3 arguments (only the key is required) :
const value setValue = ;
key
: Any string.defaultValue
(optional) : The value affected by default to the store and returned byuseStore
when no previous save is found. This could be any JSON value (and even more).assertFunction
(optional) : On initial render, or when any ofuseStore
's parameters changes, the previous save passes through this function and has to returntrue
. If it returnsfalse
or throws an error,defaultValue
will be used and overwrite the save. This can be very handy, for example to prevent the hydration ofuseStore
with ill-formed or outdated JSON. I would usually use ajv in places like these.
Identity and hook optimization
Just like most hooks, useStore
relies on object identity to optimize internal recomputations. If your defaultValue
is an object or an array, please use useRef
or useMemo
to keep the same reference as long as possible :
const init = ;const coord setCoord = ;
Similarly, use useCallback
for the assert function :
const assert = ;const state setState = ;
Better (!) : Whenever possible, set your defaultValue
and assertFunction
outside the render tree using addSchema
. This way, you don't have to worry about reference optimization. Plus, the separation between configuration and usage makes your code cleaner.
The update function
Like useState
, the update function can take a value, or a function taking the old value as an argument and returning the new one. If you update the store to the same value as the current one (using ===
for the comparison), no update is actually triggered, thus preventing useless rerenders.
const counter setCounter = <button onClick= > Increment</button> // Equivalent to : const setCounter = 1<button onClick= > Increment</button>
The identity of this update function is preserved as long as the key
stays the same.
addSchema
function
2- The This configuration function allows you to set default- default values and default assert functions to certain keys or key patterns outside of your React tree, typically in index.js
before your ReactDOM.render
. If you don't rely on props to set default values and assert functions, you shouldn't set them at component-level and addSchema
should be your primary configuration choice.
It takes the same arguments as useStore
except the key can be a regexp. If it is, then all keys matching the regexp will use the given configuration.
;;;;; ;// Any invocation of 'counter' will now use 0 as its default value, and ensure// that any retrieved save is smaller than 100. If not, 0 will be used instead. ;// Any invocation of 'coord-v1', 'coord-v43', 'coord-v9987', etc. will use the// given object as its default value. const isValidArray = ; ;// Any invocation from 'array-00' to 'array-FF' will be initialized with an// empty array. Any previous save should be an array of strings, otherwise// it will be overwritten with an empty array. ReactDOM;
config
function
3- The With this function, you can tweak some general stuff. It has to be called outside of the React structure, before any useStore
call, so usually somewhere in your index.js
before your ReactDOM.render
.
;;;; // Below are the DEFAULT settings, it is pointless// to set them explicitly to these values : ; ReactDOM;
keyPrefix
is important
Unless you have some very specific use cases, the keyPrefix
is really the only important part to configure. You set it once, and everything stored or retrieved from the storage will use that prefix in addition to the keys used in your components. All this happens of course seemlessly, you don't have to think about it.
Yes, localStorage
is compartmentalized by domain but you could have several apps by domain. It's just a good habit to set a keyPrefix
that is specific to your app. It defines a namespace.
Also, imagine several customers already tested your app and have their local copy of the store. Now say you wanna change the JSON structure because of some new requirements. You could just set keyPrefix
to the current version of the app, thus preventing any hydration of outdated JSON saves.
Here is typically what my index.js
looks like :
;;;; ; ReactDOM;
config
Schema definition with You can also replace all your addSchema
calls with a single array using the config
function :
;
readStore
function
4- The This gives you the possibility to passively read the content of your store outside of any component. This was useful for me when I needed to pass a stored token to some server requests using Apollo links. It takes only one argument, the key, and returns the corresponding JSON.
FAQ
What if no default value or assert function is set ?
Here is what's going on everytime you invoke useStore
on a specific key :
- If a previous save is found in the storage for that key, it is used. If not :
- If a default value is set locally (as an argument of
useStore
), it is used. If not : - If a default value is set globally (as an argument of
addSchema
), it is used. If not : null
will be used.
Things are easier with the assert function : if none could be found for a specific key (not locally nor globally), the functionality is simply discarded and hydration of previous saves goes without any checks.
What if you don't want your stores to be persistent ?
Since you have absolute control over the actual storage being used in the background (see config
), you can use your own custom in-memory version (it just has to implement getItem
and setItem
). There are plenty of npm packages with such alternatives. I personally like memorystorage :
;; ;
What if you are rendering server-side ?
Dead simple. Again, you have to tweak your config
to use a universal version of localStorage
(i.e. with in-memory fallback on server-side).
;; ;
What about storing non-JSON values like dates, maps and simple functions ?
This is possible with a careful use of serialize-javascript, eval
and a bit of configuration :
;; ;
About
I look forward to any suggestion, question or bug report. Please use GitHub's issue tracker.