Package Exports
- effector-storage
- effector-storage/fp
- effector-storage/fp/package.json
- effector-storage/local
- effector-storage/local/fp
- effector-storage/local/fp/package.json
- effector-storage/local/package.json
- effector-storage/memory
- effector-storage/memory/fp
- effector-storage/memory/fp/package.json
- effector-storage/memory/package.json
- effector-storage/nil
- effector-storage/nil/package.json
- effector-storage/package.json
- effector-storage/query
- effector-storage/query/fp
- effector-storage/query/fp/package.json
- effector-storage/query/package.json
- effector-storage/session
- effector-storage/session/fp
- effector-storage/session/fp/package.json
- effector-storage/session/package.json
- effector-storage/storage
- effector-storage/storage/package.json
Readme
effector-storage
Small module for Effector ☄️ to sync stores with different storages (local storage, session storage, async storage, IndexedDB, cookies, server side storage, etc).
Table of Contents
- Install
- Usage
- Usage with domains
- Functional helpers
- Formulae
createPersistfactory- Advanced usage
- Storage adapters
- FAQ
- TODO
- Sponsored
Install
$ yarn add effector-storageOr using npm
$ npm install --save effector-storageUsage
with localStorage
Docs: effector-storage/local
import { persist } from 'effector-storage/local'
// persist store `$counter` in `localStorage` with key 'counter'
persist({ store: $counter, key: 'counter' })
// if your storage has a name, you can omit `key` field
persist({ store: $counter })Stores, persisted in localStorage, are automatically synced between two (or more) windows/tabs. Also, they are synced between instances, so if you will persist two stores with the same key — each store will receive updates from another one.
with sessionStorage
Docs: effector-storage/session
Same as above, just import persist from 'effector-storage/session':
import { persist } from 'effector-storage/session'Stores, persisted in sessionStorage, are synced between instances, but not between different windows/tabs.
with query string
Docs: effector-storage/query
You can reflect plain string store value in query string parameter, using this adapter. Think of it like about synchronizing store value and query string parameter.
import { persist } from 'effector-storage/query'
// persist store `$id` in query string parameter 'id'
persist({ store: $id, key: 'id' })If two (or more) stores are persisted in query string with the same key — they are synced between themselves.
⚠️ Note
Use this only with plain string stores (Store<string | null>) to avoid strange unexpected behavior.
Usage with domains
You can use persist inside Domain's onCreateStore hook:
import { createDomain } from 'effector'
import { persist } from 'effector-storage/local'
const app = createDomain('app')
// this hook will persist every store, created in domain,
// in `localStorage`, using stores' names as keys
app.onCreateStore((store) => persist({ store }))
const $store = app.createStore(0, { name: 'store' })Functional helpers
⚠️ Due to deprecation of .thru method in effector version 22, functional helpers become obsolete, so, they are deprecated as well.
There are special persist forms to use with functional programming style. You can use them, if you like, with Domain hook or .thru() store method:
import { createDomain } from 'effector'
import { persist } from 'effector-storage/local/fp'
const app = createDomain('app')
// this hook will persist every store, created in domain,
// in `localStorage`, using stores' names as keys
app.onCreateStore(persist())
const $store = app.createStore(0, { name: 'store' })
// or persist single store in `localStorage` via .thru
const $counter = createStore(0)
.on(increment, (state) => state + 1)
.on(decrement, (state) => state - 1)
.thru(persist({ key: 'counter' }))Formulae
import { persist } from 'effector-storage/<adapter>'persist({ store, ...options }): Subscriptionpersist({ source, target, ...options }): Subscription
⚠️ Due to deprecation of .thru method in effector version 22, functional helpers become obsolete, so, they are deprecated as well.
import { persist } from 'effector-storage/<adapter>/fp'persist({ ...options }?): (store: Store) => Store
Units
In order to synchronize something, you need to specify effector units. Depending on a requirements, you may want to use store parameter, or source and target parameters:
store(Store): Store to synchronize with local/session storage.source(Event | Effect | Store): Source unit, which updates will be sent to local/session storage.target(Event | Effect | Store): Target unit, which will receive updates from local/session storage (as well as initial value). Must be different thansourceto avoid circular updates —sourceupdates are forwarded directly totarget.
Options
key? (string): Key for local/session storage, to store value in. If omitted —storename is used. Note! Ifkeyis not specified,storemust have aname! You can use'effector/babel-plugin'to have those names automatically.keyPrefix? (string): Prefix, used in adapter, to be concatenated tokey. By default =''.clock? (Event | Effect | Store): Unit, if passed – then value fromstore/sourcewill be stored in the storage only upon its trigger.pickup? (Event | Effect | Store): Unit, which you can specify to force updatestorevalue from storage.done? (Event | Effect | Store): Unit, which will be triggered on each successful read or write from/to storage.
Payload structure:fail? (Event | Effect | Store): Unit, which will be triggered in case of any error (serialization/deserialization error, storage is full and so on). Note! Iffailunit is not specified, any errors will be printed usingconsole.error(Error).
Payload structure:key(string): Samekeyas above.keyPrefix(string): Prefix, used in adapter, to be concatenated tokey. By default =''.operation('set'|'get'): Did error occurs during setting value to storage or getting value from storage.error(Error): Error instancevalue? (any): In case of 'set' operation — value fromstore. In case of 'get' operation could contain raw value from storage or could be empty.
finally? (Event | Effect | Store): Unit, which will be triggered either in case of success or error.
Payload structure:key(string): Samekeyas above.keyPrefix(string): Prefix, used in adapter, to be concatenated tokey. By default =''.operation('set'|'get'): Operation stage.status('done'|'fail'): Operation status.error? (Error): Error instance, in case of error.value? (any): Value, in case it is exists (look above).
Returns
(Subscription): You can use this subscription to remove store association with storage, if you don't need them to be synced anymore. It is a function.
(store) => Store(Function): Function, which accepts store to synchronize with storage, and returns:- (Store): Same given store.
You cannot unsubscribe store from storage when using functional form ofpersist.
- (Store): Same given store.
createPersist factory
In rare cases you might want to use createPersist factory. It allows you to specify some adapter options, like keyPrefix.
import { createPersist } from 'effector-storage/local'
const persist = createPersist({
keyPrefix: 'app/',
})
// ---8<---
persist({
store: $store1,
key: 'store1', // localStorage key will be `app/store1`
})
persist({
store: $store2,
key: 'store2', // localStorage key will be `app/store2`
})Options
keyPrefix? (string): Key prefix for adapter. It will be concatenated with anykey, given to returnedpersistfunction.
Returns
- Custom
persistfunction, with predefined adapter options.
Advanced usage
effector-storage consists of a core module and adapter modules.
The core module itself does nothing with actual storage, it just connects effector units to the storage adapter, using two Effects and bunch of forwards.
The storage adapter gets and sets values, and also can asynchronously emit values on storage updates.
import { persist } from 'effector-storage'Core function persist accepts all common options, as persist functions from sub-modules, plus additional one:
adapter(StorageAdapter): Storage adapter to use.
There is also fp form too:
import { persist } from 'effector-storage/fp'Storage adapters
Adapter is a function, which is called by the core persist function, and has following interface:
interface StorageAdapter {
<State>(key: string, update: (raw?: any) => any): {
get(raw?: any): State | Promise<State>
set(value: State): void
}
keyArea?: any
}Arguments
key(string): Unique key to distinguish values in storage.update(Function): Function, which could be called to get value from storage. In fact this isEffectwithgetfunction as a handler. In other words, any argument, passed toupdatefunction, will end up as argument ingetfunction.
Returns
{ get, set }({ Function, Function }): Getter from and setter to storage. These functions are used as Effects handlers, and could be sync or async. Also, you don't have to catch exceptions and errors inside those functions — Effects will do that for you.
As mentioned above, call ofupdatefunction will triggergetfunction with the same argument. So you can handle cases, whengetfunction is called during initialpersistexecution (without arguments), or after external update. Check out example below.
keyArea
Adapter function can have static field keyArea — this could be any value of any type, which should be unique for keys namespace. For example, two local storage adapters could have different settings, but both of them uses same storage area — localStorage. So, different stores, persisted in local storage with the same key (but possibly with different adapters), should be synced. That is what keyArea is responsible for. Value of that field is used as a key in cache Map.
In case it is omitted — adapter instances is used instead.
Synchronous storage adapter example
For example, simplified localStorage adapter might looks like this. This is over-simplified example, don't do that in real code, there are no serialization and deserialization, no checks for edge cases. This is just to show an idea.
import { createStore } from 'effector'
import { persist } from 'effector-storage'
const adapter = (key) => ({
get: () => localStorage.getItem(key),
set: (value) => localStorage.setItem(key, value),
})
const store = createStore('', { name: 'store' })
persist({ store, adapter }) // <- use adapterAsynchronous storage adapter example
Using asynchronous storage is just as simple. Once again, this is just a bare simple idea, without serialization and edge cases checks.
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createStore } from 'effector'
import { persist } from 'effector-storage'
const adapter = (key) => ({
get: async () => AsyncStorage.getItem(key),
set: async (value) => AsyncStorage.setItem(key, value),
})
const store = createStore('', { name: '@store' })
persist({ store, adapter }) // <- use adapterStorage with external updates example
If your storage can be updated from an external source, then adapter needs a way to inform/update connected store. That is where you will need second update argument.
import { createStore } from 'effector'
import { persist } from 'effector-storage'
const adapter = (key, update) => {
addEventListener('storage', (event) => {
if (event.key === key) {
// kick update
// this will call `get` function from below ↓
// wrapped in Effect, to handle any errors
update(event.newValue)
}
})
return {
// `get` function will receive `newValue` argument
// from `update`, called above ↑
get: (newValue) => newValue || localStorage.getItem(key),
set: (value) => localStorage.setItem(key, value),
}
}
const store = createStore('', { name: 'store' })
persist({ store, adapter }) // <- use adapterUpdate from non-reactive storage
If your storage can be updated from external source, and doesn't have any events to react to, but you are able to know about it somehow.
You can use optional pickup parameter to specify unit to trigger force update:
import { createEvent, createStore, forward } from 'effector'
import { persist } from 'effector-storage/session'
// event, which will be used to trigger force update
const pickup = createEvent()
const store = createStore('', { name: 'store' })
persist({ store, pickup }) // <- set `pickup` parameter
// --8<--
// when you are sure, that storage was updated,
// and you need to force update `store` from storage with new value
pickup()Another option, if you have your own adapter, you can add this feature right into it:
import { createEvent, createStore, forward } from 'effector'
import { persist } from 'effector-storage'
// event, which will be used in adapter to react to
const pickup = createEvent()
const adapter = (key, update) => {
// if `pickup` event was triggered -> trigger `update`
// this will call `get` function from below ↓
// wrapped in Effect, to handle any errors
forward({ from: pickup, to: update })
return {
get: () => localStorage.getItem(key),
set: (value) => localStorage.setItem(key, value),
}
}
const store = createStore('', { name: 'store' })
persist({ store, adapter }) // <- use your adapter
// --8<--
// when you are sure, that storage was updated,
// and you need to force update `store` from storage with new value
pickup()Local storage adapter with values expiration
I want sync my store with
localStorage, but I need smart synchronization, not dumb. Each storage update should contain last write timestamp. And on read value I need to check if value has been expired, and fill store with default value in that case.
You can implement it with custom adapter, something like this:
import { createStore } from 'effector'
import { persist } from 'effector-storage'
const adapter = (timeout) => (key) => ({
get() {
const item = localStorage.getItem(key)
if (item === null) return // no value in localStorage
const { time, value } = JSON.parse(item)
if (time + timeout * 1000 < Date.now()) return // value has expired
return value
},
set(value) {
localStorage.setItem(key, JSON.stringify({ time: Date.now(), value }))
},
})
const store = createStore('', { name: 'store' })
// use adapter with timeout = 1 hour ↓↓↓
persist({ store, adapter: adapter(3600) })Custom Storage adapter
Both 'effector-storage/local' and 'effector-storage/session' are using common storage adapter factory. If you want to use other storage, which implements Storage interface (in fact, synchronous getItem and setItem methods are enough) — you can use this factory.
import { storage } from 'effector-storage/storage'adapter = storage(options)Options
storage(Storage): Storage to communicate with.sync? (boolean): Add'storage'event listener or no. Default =false.serialize? ((value: any) => string): Custom serialize function. Default =JSON.stringifydeserialize? ((value: string) => any): Custom deserialize function. Default =JSON.parse
Returns
- (StorageAdapter): Storage adapter, which can be used with the core
persistfunction.
FAQ
Can I persist part of the store?
The issue here is that it is hardly possible to create universal mapping to/from storage to the part of the store within the library implementation. But with persist form with source/target, and little help of Effector API you can make it:
import { persist } from 'effector-storage/local'
const setX = createEvent()
const setY = createEvent()
const $coords = createStore({ x: 123, y: 321 })
.on(setX, ({ y }, x) => ({ x, y }))
.on(setY, ({ x }, y) => ({ x, y }))
// persist X coordinate in `localStorage` with key 'x'
persist({
source: $coords.map(({ x }) => x),
target: setX,
key: 'x',
})
// persist Y coordinate in `localStorage` with key 'y'
persist({
source: $coords.map(({ y }) => y),
target: setY,
key: 'y',
})⚠️ BIG WARNING!
Use this approach with caution, beware of infinite circular updates. To avoid them, persist only plain values in storage. So, mapped store in source will not trigger update, if object in original store has changed. Also, you can take a look at updateFilter option.
TODO
- localStorage support (docs: effector-storage/local)
- sessionStorage support (docs: effector-storage/session)
- query string support (docs: effector-storage/query)
- IndexedDB support
- AsyncStorage support
- Cookies support
- you name it support
