pathstore-react v0.0.6
Pathstore
A simple, performant global store that works well with React.
✨ Also works with Redux Devtools ✨
Why does this exist?
Wanted a global store that is:
- performant / can scale
- tiny
- succinct
Table of Contents
From React local state to Pathstore
Suppose we have a simple counter component that uses local react state:
const Counter = () => {
const [count, setCount] = useState(0)
return <div>
<button onClick={() => setCount(count + 1)} >Increment</button>
<span>count: {count}</span>
</div>
}To use Pathstore instead of local state, replace useState(0) with store.use(['counter'], 0). Your component should look like this.
const Counter = () => {
const [count, setCount] = store.use(['counter'], 0)
return <div>
<button onClick={() => setCount(count + 1)} >Increment</button>
<span>count: {count}</span>
</div>
}Now the counter value is stored in the global state {..., counter: <value>, ...} and its value can easily be used in other components.
You might wonder, why did we pass in ['counter'] instead of just 'counter'. This is because Pathstore lets you use nested values just as easily as any other values. For example, if instead of ['counter'] we pass in ['counter', 'nestedExample'], then the value of the counter in the store would look something like this:
{
counter: {
nestedExample: <value>,
...
},
...
}Getting started
install
npm install --save pathstore-reactcreate a store
import {createStore} from 'pathstore-react'
import {useState, useRef, useEffect} from 'react'
export const store = createStore({ useEffect, useState, useRef, reduxDevtools: true })use the store
Examples
Table of Contents
Form Input
// This will be a lot more satisfying if you have Redux Devtools running in your browser...
const TextInput = ({formName, name, defaultValue = '', ...props}) => {
const [value, setValue] = store.use([formName, 'values', name], defaultValue)
return <input
onChange={ev => setValue(ev.target.value)}
name={name}
value={value}
{...props}
/>
}
const FieldError = ({formName, name, ...props}) => {
const [value] = store.use([formName, 'errors', name])
return value ? <span {...props}>{value}</span> : null
}
const ExampleForm = () => {
const name = 'ExampleForm'
const onSubmit = ev => {
ev.preventDefault()
const values = store.get([name, 'values'])
// from here you can run some validations, submit the values, etc.
// ...
// As an example, lets say there's an email field error:
store.set([name, 'errors', 'email'], 'A fake email error')
return
}
return <form onSubmit={onSubmit}>
<TextInput formName={name} name='email' type='email' />
<FieldError formName={name} name='email' />
<TextInput formName={name} name='password' type='password' />
<FieldError formName={name} name='password' />
<button>Submit</button>
</form>
}Initialization
Often you need to set some initial state before your app even starts, and maybe again when the user logs out.
const initStore = (store) => {
store.set([], {
token: localStorage.getItem('token'),
theme: localStorage.getItem('theme')
})
}
const store = createStore(...)
initStore(store)Many updates at once
Sometimes you want to change state in more than once place but you only want your components to rerender after all the changes are made. There's a noPublish option for that.
const onSubmit = (ev) => {
// ...
store.set(['modalState'], undefined, {noPublish: true})
store.set(['modal'], undefined)
// subscriptions will only be called after the second store.set is called
}Performance
Improving performance of global stores was one of the main reasons Pathstore was built. Global stores often call every subscriber for every state change. It's basically asking every stateful component "Do you care about this change?" for every single state change. This becomes a problem if you're storing things that can change many times in a short period of time (like mouse position). This doesn't seem optimal. With Pathstore, subscribers can subscribe to a specific location in the store, which could cut down significantly on the number of times it's called.
I haven't gotten the chance to benchmark Pathstore vs alternatives like Redux and Unistore. Not even sure the best way to do this. If anyone has ideas, please let me know by creating an issue.
Pathstore is also quite small, for those concerned with initial load times.
API
Table of Contents
createStore
Creates a new store.
Parameters
initObject The initialization object.useStateFunction The useState function from react or preact.useRefFunction The useRef function from react or preact.reduxDevtoolsBoolean Whether to turn on redux devtools.
Returns
storeObject A store
Examples
import { useState, useRef } from 'react'
let store = createStore({useState, useRef, reduxDevtools: true})
store.subscribe([], state => console.log(state) );
store.set(['a'], 'b') // logs { a: 'b' }
store.set(['c'], 'd') // logs { a: 'b', c: 'd' }store
An observable state container, returned from createStore
store.set
A function for changing a value in the store's state. Will call all subscribe functions of changed path.
Parameters
pathArray The path to set.valueAny The new value. If you provide a function, it will be given the current value at path and should return a new value. (see examples).optionsObject (optional) Some additional options.noPublishBoolean (optional) Do not trigger subscriber functions. The subscribe functions that would have been called will instead be called the next timestore.setis called without thenoPublishoptionidentifierString (optional) A custom identifier that will be shown in Redux Devtools. Normally in Redux-land this would be the action. In Pathstore this is normally the path.setByFunctionBoolean (optional, defaultfalse) If passing a function in as thevalueargument, the function will be called with the current value at the path. If this isfalsethen the function passed in will be treated as a value and saved directly in the store.
Examples
store.set([], {}) // the store is {}
store.set(['a'], 1) // the store is {a: 1}
store.set(['a'], x => x + 1, {setByFunction: true}) // the store is {a: 2}
store.set(['b', 0, 'c'], 1) // the store is {a: 2, b: [{c: 1}]}store.get
A function for retrieving values in the store's state.
Parameters
pathArray The path to use.
Returns
valueAny The value atpath.
Examples
store.set([], {a: 1, b: {c: 'd'}, e: ['f', 'g', 'h']})
store.get(['a']) // => 1
store.get(['b', 'c']) // => 'd'
store.get(['e', 1]) // => 'g'
store.get(['e', 4]) // => undefined
store.get(['z']) // => undefined
store.get([]) // => {a: 1, b: {c: 'd'}, e: ['f', 'g', 'h']}store.subscribe
Add a function to be called whenever state changes anywhere along the specified path.
Parameters
pathArray The path to use.subscriberFunction The function to call when state changes along the path.
Returns
unsubscribeFunction Stopsubscriberfrom being called anymore.
Examples
Subscribe to any state changes
let unsub = store.subscribe([], () => console.log('state has changed') );
store.set(['a'], 'b') // logs 'state has changed'
store.set(['c'], 'd') // logs 'state has changed'
store.set([], {}) // logs 'state has changed'
unsub() // stop our function from being called
store.set(['a'], 3) // does not log anythingSubscribe to a specific path in state
let unsub = store.subscribe(['a', 'b', 'c'], () => console.log('a.b.c state has changed') );
store.set([], {a: {b: {c: 4}}}) // logs 'a.b.c state has changed'
store.set(['a', 'b', 'c'], 5) // logs 'a.b.c state has changed'
store.set(['b'], 5) // does not log anything
store.set(['a', 'b', 'd'], 2) // does not log anything
store.set(['a', 'b', 'c', 'd', 'e'], 2) // logs 'a.b.c state has changed'
store.set([], {x: 123}) // logs 'a.b.c state has changed'store.use
Hook that returns a stateful value, and a function to update it.
Parameters
pathArray The path to use.initialValueAny (optional) The initial value.optionsObject (optional) Some additional options.cleanupBoolean (optional, defaultfalse) Set the value atpathin state toundefinedwhen the component unmounts.overrideBoolean (optional, defaultfalse) Set the value atpathtoinitialValueeven if there is already a value there.identifierString (optional) An identifier to use in Redux Devtools.setByFunctionBoolean (optional, defaultfalse) Passing a function in to thesetfunction returned bystore.use, the function will be called with the current value at the path. If this isfalsethen the function passed in will be treated as a value and saved directly in the store.
Return
[value, setValue]ArrayvalueAny The value atpathsetValueFunction Set a new value at path
Examples
A counter component, the value of the counter will be stored in state
under {counter: <here>}
const Counter = () => {
const [count, setCount] = store.use(['counter'], 0)
return <div>
<button onClick={() => setCount(count + 1)} >Increment</button>
<span>count: {count}</span>
</div>
}The same component, but storing the count under {counter: {nested: <here>}}
const Counter = () => {
const [count, setCount] = store.use(['counter', 'nested'], 0)
return <div>
<button onClick={() => setCount(count + 1)} >Increment</button>
<span>count: {count}</span>
</div>
}This time storing the count under a dynamic id key {counter: {<id>: <here>}}
const Counter = ({id}) => {
const [count, setCount] = store.use(['counter', id], 0)
return <div>
<button onClick={() => setCount(count + 1)} >Increment</button>
<span>count: {count}</span>
</div>
}Using the cleanup option. When the component unmounts the value will be set to undefined, {counter: undefined}
const Counter = () => {
const [count, setCount] = store.use(['counter'], 0, {cleanup: true})
return <div>
<button onClick={() => setCount(count + 1)} >Increment</button>
<span>count: {count}</span>
</div>
}Using the override option. When the component mounts the value will be set to initialValue, even if there is already a value in state. count will be 0 because the current value 4 is overriden with the initial value 0
store.set([], {counter: 4})
const Counter = () => {
const [count, setCount] = store.use(['counter'], 0, {override: true})
return <div>
<button onClick={() => setCount(count + 1)} >Increment</button>
<span>count: {count} will always start at 0</span>
</div>
}