Installations
npm install effector-storage
Developer Guide
Typescript
Yes
Module System
ESM
Node Version
20.11.1
NPM Version
10.2.4
Score
92.1
Supply Chain
96.6
Quality
77.1
Maintenance
100
Vulnerability
100
License
Releases
Contributors
Unable to fetch Contributors
Languages
TypeScript (91.9%)
JavaScript (8.02%)
HTML (0.08%)
Developer
yumauri
Download Statistics
Total Downloads
802,617
Last Day
1,424
Last Week
9,159
Last Month
43,381
Last Year
407,143
GitHub Statistics
106 Stars
398 Commits
3 Forks
2 Watching
13 Branches
5 Contributors
Bundle Size
7.00 kB
Minified
2.56 kB
Minified + Gzipped
Package Meta Information
Latest Version
7.1.0
Package Id
effector-storage@7.1.0
Unpacked Size
223.09 kB
Size
46.85 kB
File Count
98
NPM Version
10.2.4
Node Version
20.11.1
Publised On
10 Mar 2024
Total Downloads
Cumulative downloads
Total Downloads
802,617
Last day
-46.1%
1,424
Compared to previous day
Last week
-21.3%
9,159
Compared to previous week
Last month
29.4%
43,381
Compared to previous month
Last year
79.2%
407,143
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
Peer Dependencies
1
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
- Formulae
createPersist
factory- Advanced usage
- Storage adapters
- FAQ
- TODO
- Sponsored
Install
Depending on your package manager
1# using `pnpm` ↓ 2$ pnpm add effector-storage 3 4# using `yarn` ↓ 5$ yarn add effector-storage 6 7# using `npm` ↓ 8$ npm install --save effector-storage
Usage
with localStorage
Docs: effector-storage/local
1import { persist } from 'effector-storage/local' 2 3// persist store `$counter` in `localStorage` with key 'counter' 4persist({ store: $counter, key: 'counter' }) 5 6// if your storage has a name, you can omit `key` field 7persist({ 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.
ℹ️ If you need just basic bare minimum functionality, you can take a look at effector-localstorage
library. It has similar API, it much simpler and tinier.
with sessionStorage
Docs: effector-storage/session
Same as above, just import persist
from 'effector-storage/session'
:
1import { 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.
1import { persist } from 'effector-storage/query' 2 3// persist store `$id` in query string parameter 'id' 4persist({ store: $id, key: 'id' })
If two (or more) stores are persisted in query string with the same key — they are synced between themselves.
with BroadcastChannel
Docs: effector-storage/broadcast
You can sync stores across different browsing contexts (tabs, windows, workers), just import persist
from 'effector-storage/broadcast'
:
1import { persist } from 'effector-storage/broadcast'
extra adapters
You can find a collection of useful adapters in effector-storage-extras. That side repository was created in order to not bloat effector-storage
with dependencies and adapters, which depends on other libraries.
Usage with domains
You can use persist
inside Domain's onCreateStore
hook:
1import { createDomain } from 'effector' 2import { persist } from 'effector-storage/local' 3 4const app = createDomain('app') 5 6// this hook will persist every store, created in domain, 7// in `localStorage`, using stores' names as keys 8app.onCreateStore((store) => persist({ store })) 9 10const $store = app.createStore(0, { name: 'store' })
Formulae
1import { persist } from 'effector-storage/<adapter>'
persist({ store, ...options }): Subscription
persist({ source, target, ...options }): Subscription
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 thansource
to avoid circular updates —source
updates are passed directly totarget
.
Options
key
? (string): Key for local/session storage, to store value in. If omitted —store
name is used. Note! Ifkey
is not specified,store
must 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
/source
will be stored in the storage only upon its trigger.pickup
? (Event | Effect | Store): Unit, which you can specify to updatestore
value from storage. This unit can also set a special context for adapter. Note! When you addpickup
,persist
will not get initial value from storage automatically!context
? (Event | Effect | Store): Unit, which can set a special context for adapter.contract
? (Contract): Rule to statically validate data 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! Iffail
unit is not specified, any errors will be printed usingconsole.error(Error)
.
Payload structure:key
(string): Samekey
as above.keyPrefix
(string): Prefix, used in adapter, to be concatenated tokey
. By default =''
.operation
('set'
|'get'
|'validate'
): Type of operation, read (get), write (set) or validation against contract (validate).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): Samekey
as above.keyPrefix
(string): Prefix, used in adapter, to be concatenated tokey
. By default =''
.operation
('set'
|'get'
|'validate'
): Type of operation, read (get), write (set) or validation against contract (validate).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.
Contracts
You can use contract
option to validate data from storage. Contract has the following type definition:
1export type Contract<Data> = 2 | ((raw: unknown) => raw is Data) 3 | { 4 isData: (raw: unknown) => raw is Data 5 getErrorMessages: (raw: unknown) => string[] 6 }
So, it could be simple type guard function in trivial use cases, or more complex object with isData
type guard and getErrorMessages
function, which returns array of error messages. This format is fully compatible with Farfetched contracts, so you can use any adapter from Farfetched (runtypes, zod, io-ts, superstruct, typed-contracts) with persist
and contract
option:
1// simple type guard 2persist({ 3 store: $counter, 4 key: 'counter', 5 contract: (raw): raw is number => typeof raw === 'number', 6})
1// complex contract with Farfetched adapter 2import { Record, Literal, Number } from 'runtypes' 3import { runtypeContract } from '@farfetched/runtypes' 4 5const Asteroid = Record({ 6 type: Literal('asteroid'), 7 mass: Number, 8}) 9 10persist({ 11 store: $asteroid, 12 key: 'asteroid', 13 contract: runtypeContract(Asteroid), 14})
There are two gotchas with contracts:
- From
effector-storage
point of view it is absolutely normal, when there is no persisted value in the storage yet. So,undefined
value is always valid, even if contract does not explicitly allow it. effector-storage
does not prevent persisting invalid data to the storage, but it will validate it nonetheless, after persisting, so, if you write invalid data to the storage,fail
will be triggered, but data will be persisted.
Notes
Without specifying pickup
property, calling persist
will immediately call adapter to get initial value. In case of synchronous storage (like localStorage
or sessionStorage
) this action will synchronously set store value, and call done
/fail
/finally
right away. You should take that into account, if you adds some logic on done
, for example — place persist
after that logic (see issue #38 for more details).
You can modify adapter to be asynchronous to mitigate this behavior with async
function.
createPersist
factory
In rare cases you might want to use createPersist
factory. It allows you to specify some adapter options, like keyPrefix
.
1import { createPersist } from 'effector-storage/local' 2 3const persist = createPersist({ 4 keyPrefix: 'app/', 5}) 6 7// ---8<--- 8 9persist({ 10 store: $store1, 11 key: 'store1', // localStorage key will be `app/store1` 12}) 13persist({ 14 store: $store2, 15 key: 'store2', // localStorage key will be `app/store2` 16})
Options
pickup
? (Event | Effect | Store): Unit, which you can specify to updatestore
value from storage. This unit can also set a special context for adapter. Note! When you addpickup
,persist
will not get initial value from storage automatically!context
? (Event | Effect | Store): Unit, which can set a special context for adapter.keyPrefix
? (string): Key prefix for adapter. It will be concatenated with anykey
, given to returnedpersist
function.contract
? (Contract): Rule to statically validate data from storage.
Returns
- Custom
persist
function, 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 couple of Effects and bunch of connections.
The storage adapter gets and sets values, and also can asynchronously emit values on storage updates.
1import { 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.
Storage adapters
Adapter is a function, which is called by the core persist
function, and has following interface:
1interface StorageAdapter { 2 <State>( 3 key: string, 4 update: (raw?: any) => void 5 ): { 6 get(raw?: any, ctx?: any): State | Promise<State | undefined> | undefined 7 set(value: State, ctx?: any): void 8 } 9 keyArea?: any 10 noop?: boolean 11}
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 isEffect
withget
function as a handler. In other words, any argument, passed toupdate
function, will end up as argument inget
function.
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 ofupdate
function will triggerget
function with the same argument. So you can handle cases, whenget
function is called during initialpersist
execution (without arguments), or after external update. Check out example below.
Also getter and setter both accepts optional context as a second argument — it can be any value. This context could be useful, if adapter depends on some external environment, for example, it can contain Request and Response from Express middleware, to get/set cookies from/to. (TODO: isomorphic cookies adapter example).
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.
noop
Marks adapter as "no-op" for either
function.
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.
1import { createStore } from 'effector' 2import { persist } from 'effector-storage' 3 4const adapter = (key) => ({ 5 get: () => localStorage.getItem(key), 6 set: (value) => localStorage.setItem(key, value), 7}) 8 9const store = createStore('', { name: 'store' }) 10persist({ store, adapter }) // <- use adapter
Asynchronous 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. If you need to use React Native Async Storage, try @effector-storage/react-native-async-storage) adapter instead.
1import AsyncStorage from '@react-native-async-storage/async-storage' 2import { createStore } from 'effector' 3import { persist } from 'effector-storage' 4 5const adapter = (key) => ({ 6 get: async () => AsyncStorage.getItem(key), 7 set: async (value) => AsyncStorage.setItem(key, value), 8}) 9 10const store = createStore('', { name: '@store' }) 11persist({ store, adapter }) // <- use adapter
Storage 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.
1import { createStore } from 'effector' 2import { persist } from 'effector-storage' 3 4const adapter = (key, update) => { 5 addEventListener('storage', (event) => { 6 if (event.key === key) { 7 // kick update 8 // this will call `get` function from below ↓ 9 // wrapped in Effect, to handle any errors 10 update(event.newValue) 11 } 12 }) 13 14 return { 15 // `get` function will receive `newValue` argument 16 // from `update`, called above ↑ 17 get: (newValue) => newValue || localStorage.getItem(key), 18 set: (value) => localStorage.setItem(key, value), 19 } 20} 21 22const store = createStore('', { name: 'store' }) 23persist({ store, adapter }) // <- use adapter
Update 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 update (keep in mind, that when you add pickup
, persist
will not get initial value from storage automatically):
1import { createEvent, createStore } from 'effector' 2import { persist } from 'effector-storage/session' 3 4// event, which will be used to trigger update 5const pickup = createEvent() 6 7const store = createStore('', { name: 'store' }) 8persist({ store, pickup }) // <- set `pickup` parameter 9 10// --8<-- 11 12// when you are sure, that storage was updated, 13// and you need to update `store` from storage with new value 14pickup()
Another option, if you have your own adapter, you can add this feature right into it:
1import { createEvent, createStore } from 'effector' 2import { persist } from 'effector-storage' 3 4// event, which will be used in adapter to react to 5const pickup = createEvent() 6 7const adapter = (key, update) => { 8 // if `pickup` event was triggered -> call an `update` function 9 // this will call `get` function from below ↓ 10 // wrapped in Effect, to handle any errors 11 pickup.watch(update) 12 return { 13 get: () => localStorage.getItem(key), 14 set: (value) => localStorage.setItem(key, value), 15 } 16} 17 18const store = createStore('', { name: 'store' }) 19persist({ store, adapter }) // <- use your adapter 20 21// --8<-- 22 23// when you are sure, that storage was updated, 24// and you need to force update `store` from storage with new value 25pickup()
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:
1import { createStore } from 'effector' 2import { persist } from 'effector-storage' 3 4const adapter = (timeout) => (key) => ({ 5 get() { 6 const item = localStorage.getItem(key) 7 if (item === null) return // no value in localStorage 8 const { time, value } = JSON.parse(item) 9 if (time + timeout * 1000 < Date.now()) return // value has expired 10 return value 11 }, 12 13 set(value) { 14 localStorage.setItem(key, JSON.stringify({ time: Date.now(), value })) 15 }, 16}) 17 18const store = createStore('', { name: 'store' }) 19 20// use adapter with timeout = 1 hour ↓↓↓ 21persist({ 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.
1import { storage } from 'effector-storage/storage'
1adapter = storage(options)
Options
storage
(Storage): Storage to communicate with.sync
? (boolean | 'force'): Add'storage'
event listener or no. Default =false
. In case of'force'
value adapter will always read new value from Storage, instead of event.serialize
? ((value: any) => string): Custom serialize function. Default =JSON.stringify
deserialize
? ((value: string) => any): Custom deserialize function. Default =JSON.parse
Returns
- (StorageAdapter): Storage adapter, which can be used with the core
persist
function.
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:
1import { persist } from 'effector-storage/local' 2 3const setX = createEvent() 4const setY = createEvent() 5const $coords = createStore({ x: 123, y: 321 }) 6 .on(setX, ({ y }, x) => ({ x, y })) 7 .on(setY, ({ x }, y) => ({ x, y })) 8 9// persist X coordinate in `localStorage` with key 'x' 10persist({ 11 source: $coords.map(({ x }) => x), 12 target: setX, 13 key: 'x', 14}) 15 16// persist Y coordinate in `localStorage` with key 'y' 17persist({ 18 source: $coords.map(({ y }) => y), 19 target: setY, 20 key: 'y', 21})
⚠️ 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)
- BroadcastChannel support (docs: effector-storage/broadcast)
- AsyncStorage support (extras: @effector-storage/react-native-async-storage)
- EncryptedStorage support (extras: @effector-storage/react-native-encrypted-storage)
- IndexedDB support (extras: @effector-storage/idb-keyval)
- Cookies support
- you name it support
Sponsored
No vulnerabilities found.
Reason
no dangerous workflow patterns detected
Reason
no binaries found in the repo
Reason
license file detected
Details
- Info: project has a license file: LICENSE:0
- Info: FSF or OSI recognized license: MIT License: LICENSE:0
Reason
1 existing vulnerabilities detected
Details
- Warn: Project is vulnerable to: GHSA-mwcw-c2x4-8c55
Reason
1 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 1
Reason
Found 0/24 approved changesets -- score normalized to 0
Reason
detected GitHub workflow tokens with excessive permissions
Details
- Info: jobLevel 'contents' permission set to 'read': .github/workflows/build.yml:17
- Warn: jobLevel 'packages' permission set to 'write': .github/workflows/build.yml:18
- Warn: no topLevel permission defined: .github/workflows/build.yml:1
- Warn: no topLevel permission defined: .github/workflows/playwright.yml:1
- Warn: no topLevel permission defined: .github/workflows/publish.yml:1
- Warn: no topLevel permission defined: .github/workflows/try.yml:1
- Warn: no topLevel permission defined: .github/workflows/try_all.yml:1
Reason
dependency not pinned by hash detected -- score normalized to 0
Details
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yml:24: update your workflow using https://app.stepsecurity.io/secureworkflow/yumauri/effector-storage/build.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yml:30: update your workflow using https://app.stepsecurity.io/secureworkflow/yumauri/effector-storage/build.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/playwright.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/yumauri/effector-storage/playwright.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/playwright.yml:25: update your workflow using https://app.stepsecurity.io/secureworkflow/yumauri/effector-storage/playwright.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/yumauri/effector-storage/publish.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:26: update your workflow using https://app.stepsecurity.io/secureworkflow/yumauri/effector-storage/publish.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/try.yml:44: update your workflow using https://app.stepsecurity.io/secureworkflow/yumauri/effector-storage/try.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/try.yml:61: update your workflow using https://app.stepsecurity.io/secureworkflow/yumauri/effector-storage/try.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/try.yml:67: update your workflow using https://app.stepsecurity.io/secureworkflow/yumauri/effector-storage/try.yml/main?enable=pin
- Info: 0 out of 8 GitHub-owned GitHubAction dependencies pinned
- Info: 0 out of 1 third-party GitHubAction dependencies pinned
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
security policy file not detected
Details
- Warn: no security policy file detected
- Warn: no security file to analyze
- Warn: no security file to analyze
- Warn: no security file to analyze
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Reason
branch protection not enabled on development/release branches
Details
- Warn: branch protection not enabled for branch 'main'
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
- Warn: 0 commits out of 8 are checked with a SAST tool
Score
3.4
/10
Last Scanned on 2024-12-16
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More