Pathstore
A simple, performant global store that works well with React.
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 @adriaanwm/pathstore
create a store
import {createStore} from '@adriaanwm/pathstore'
import {useState, useRef} from 'react'
export const store = createStore({ 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
-
init
Object The initialization object.-
useState
Function The useState function from react or preact. -
useRef
Function The useRef function from react or preact. -
reduxDevtools
Boolean Whether to turn on redux devtools.
-
Returns
-
store
Object 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
-
path
Array The path to set. -
value
Any 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). -
options
Object (optional) Some additional options.-
noPublish
Boolean (optional) Do not trigger subscriber functions. The subscribe functions that would have been called will instead be called the next timestore.set
is called without thenoPublish
option -
identifier
String (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.
-
Examples
store.set([], {}) // the store is {}
store.set(['a'], 1) // the store is {a: 1}
store.set(['a'], x => x + 1) // 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
-
path
Array The path to use.
Returns
-
value
Any 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
-
path
Array The path to use. -
subscriber
Function The function to call when state changes along the path.
Returns
-
unsubscribe
Function Stopsubscriber
from 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 anything
Subscribe 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
-
path
Array The path to use. -
initialValue
Any (optional) The initial value. -
options
Object (optional) Some additional options.-
cleanup
Boolean (optional, defaultfalse
) Set the value atpath
in state toundefined
when the component unmounts. -
override
Boolean (optional, defaultfalse
) Set the value atpath
toinitialValue
even if there is already a value there. -
identifier
String (optional) An identifier to use in Redux Devtools.
-
Return
-
[value, setValue]
Array-
value
Any The value atpath
-
setValue
Function 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>
}