Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified docs/bun.lockb
Binary file not shown.
31 changes: 24 additions & 7 deletions docs/src/content/docs/reference/synchronizer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,35 @@ const { useStore } = createStore({
})
```

#### Custom serializer
#### Custom storage

If you want to store more complex objects that aren't supported by JSON you can either write your own storage synchronizer or pass custom `serialize` and `deserialize` functions to the options' parameter. For example, you can use [superjson](https://github.com/blitz-js/superjson) package:
If you want to customize storage, for example to use your own mmkv instance - you can use `createStorage` factory function. It creates a new storage function that you can use across your app.

```ts
import { createStorage } from 'stan-js'
import { MMKV } from 'react-native-mmkv'

const encryptedMMKV = new MMKV({
encryptionKey: 'my-encryption-key'
})

const myStorage = createStorage({
mmkvInstance: encryptedMMKV,
})
```

Or if you want to store more complex objects that aren't supported by JSON you can pass custom `serialize` and `deserialize` functions. For example, you can use [superjson](https://github.com/blitz-js/superjson) package:
```typescript
import { createStore } from 'stan-js'
import { storage } from 'stan-js/storage'
import { createStorage } from 'stan-js/storage'
import superjson from 'superjson'

const superJsonStorage = createStorage({
serialize: superjson.stringify,
deserialize: superjson.deserialize
})

const { useStore } = createStore({
user: storage(new Set(), {
serialize: superjson.stringify,
deserialize: superjson.deserialize
})
user: superJsonStorage(new Set())
})
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
"./storage": {
"module": "./dist/storage/index.mjs",
"react-native": "./dist/storage/mmkv.cjs",
"react-native": "./dist/storage/native.cjs",
"browser": "./dist/storage/index.cjs",
"import": {
"types": "./dist/storage/index.d.mts",
Expand Down
96 changes: 96 additions & 0 deletions src/storage/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { CreateStorageOptions, Storage, StorageOptions, Synchronizer } from '../types'

export const isLocalStorageAvailable = () => {
// for SSR
if (typeof window === 'undefined') {
return false
}

try {
window.localStorage.length

return true
} catch {
// if localStorage is disabled
return false
}
}
const mapStorage = new Map<string, string>()

const ssrSaveStorage = {
setItem: (key: string, value: string) => {
if (isLocalStorageAvailable()) {
localStorage.setItem(key, value)

return
}

mapStorage.set(key, value)
},
getItem: (key: string) => {
if (isLocalStorageAvailable()) {
return localStorage.getItem(key)
}

return mapStorage.get(key)
},
removeItem: (key: string) => {
if (isLocalStorageAvailable()) {
localStorage.removeItem(key)

return
}

mapStorage.delete(key)
},
}

export const createStorage = (storageOptions?: CreateStorageOptions): Storage =>
<T>(
initialValue: T,
{
deserialize = storageOptions?.deserialize ?? JSON.parse,
serialize = storageOptions?.serialize ?? JSON.stringify,
storageKey,
}: StorageOptions<T> = {},
) => ({
value: initialValue,
subscribe: (update, key) => {
const handleEvent = (event: StorageEvent) => {
if (event.storageArea !== localStorage) {
return
}

if (event.key !== (storageKey ?? key) || event.newValue === null) {
return
}

update(deserialize(event.newValue))
}

if (!isLocalStorageAvailable()) {
return
}

window.addEventListener('storage', handleEvent)
},
update: (value, key) => {
if (value === undefined) {
ssrSaveStorage.removeItem(storageKey ?? key)

return
}

ssrSaveStorage.setItem(storageKey ?? key, serialize(value))
},
getSnapshot: key => {
const value = ssrSaveStorage.getItem(storageKey ?? key)

if (value === null || value === undefined) {
// Value is not in storage
throw new Error()
}

return deserialize(value)
},
} as Synchronizer<T>)
1 change: 1 addition & 0 deletions src/storage/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { createStorage } from './factory'
export { storage } from './storage'
37 changes: 2 additions & 35 deletions src/storage/mmkv.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,3 @@
import { MMKV } from 'react-native-mmkv'
import { Storage, StorageOptions, Synchronizer } from '../types'
import { createStorage } from './mmkvFactory'

const mmkv = new MMKV()

export const storage: Storage = <T>(
initialValue: T,
{
deserialize = JSON.parse,
serialize = JSON.stringify,
storageKey,
}: StorageOptions<T> = {},
) => ({
value: initialValue,
update: (value, key) => {
const storageKeyToUse = storageKey ?? key

if (value === undefined) {
mmkv.delete(storageKeyToUse)

return
}

mmkv.set(storageKeyToUse, serialize(value))
},
getSnapshot: key => {
const value = mmkv.getString(storageKey ?? key)

if (value === undefined) {
// Value is not in storage
throw new Error()
}

return deserialize(value)
},
} as Synchronizer<T>)
export const storage = createStorage()
39 changes: 39 additions & 0 deletions src/storage/mmkvFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { MMKV } from 'react-native-mmkv'
import { CreateStorageOptions, StorageOptions, Synchronizer } from '../types'

export const createStorage = (storageOptions?: CreateStorageOptions) =>
<T>(
initialValue: T,
{
deserialize = storageOptions?.deserialize ?? JSON.parse,
serialize = storageOptions?.serialize ?? JSON.stringify,
storageKey,
}: StorageOptions<T> = {},
) => {
const mmkv = storageOptions?.mmkvInstance ?? new MMKV()

return {
value: initialValue,
update: (value, key) => {
const storageKeyToUse = storageKey ?? key

if (value === undefined) {
mmkv.delete(storageKeyToUse)

return
}

mmkv.set(storageKeyToUse, serialize(value))
},
getSnapshot: key => {
const value = mmkv.getString(storageKey ?? key)

if (value === undefined) {
// Value is not in storage
throw new Error()
}

return deserialize(value)
},
} as Synchronizer<T>
}
2 changes: 2 additions & 0 deletions src/storage/native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { storage } from './mmkv'
export { createStorage } from './mmkvFactory'
96 changes: 2 additions & 94 deletions src/storage/storage.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,4 @@
import { Storage, StorageOptions, Synchronizer } from '../types'

export const isLocalStorageAvailable = () => {
// for SSR
if (typeof window === 'undefined') {
return false
}

try {
window.localStorage.length

return true
} catch {
// if localStorage is disabled
return false
}
}
const mapStorage = new Map<string, string>()

const ssrSaveStorage = {
setItem: (key: string, value: string) => {
if (isLocalStorageAvailable()) {
localStorage.setItem(key, value)

return
}

mapStorage.set(key, value)
},
getItem: (key: string) => {
if (isLocalStorageAvailable()) {
return localStorage.getItem(key)
}

return mapStorage.get(key)
},
removeItem: (key: string) => {
if (isLocalStorageAvailable()) {
localStorage.removeItem(key)

return
}

mapStorage.delete(key)
},
}
import { createStorage } from './factory'

/**
* A Synchronizer implementation using localStorage and mmkv
Expand All @@ -53,51 +8,4 @@ const ssrSaveStorage = {
* @param options.storageKey - key to use in storage
* @see {@link https://codemask-labs.github.io/stan-js/reference/synchronizer/}
*/
export const storage: Storage = <T>(
initialValue: T,
{
deserialize = JSON.parse,
serialize = JSON.stringify,
storageKey,
}: StorageOptions<T> = {},
) => ({
value: initialValue,
subscribe: (update, key) => {
const handleEvent = (event: StorageEvent) => {
if (event.storageArea !== localStorage) {
return
}

if (event.key !== (storageKey ?? key) || event.newValue === null) {
return
}

update(deserialize(event.newValue))
}

if (!isLocalStorageAvailable()) {
return
}

window.addEventListener('storage', handleEvent)
},
update: (value, key) => {
if (value === undefined) {
ssrSaveStorage.removeItem(storageKey ?? key)

return
}

ssrSaveStorage.setItem(storageKey ?? key, serialize(value))
},
getSnapshot: key => {
const value = ssrSaveStorage.getItem(storageKey ?? key)

if (value === null || value === undefined) {
// Value is not in storage
throw new Error()
}

return deserialize(value)
},
} as Synchronizer<T>)
export const storage = createStorage()
4 changes: 2 additions & 2 deletions src/tests/storage.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, jest, Mock, spyOn } from 'bun:test'
import { storage } from '../storage'
import { isLocalStorageAvailable } from '../storage/storage'
import { isLocalStorageAvailable } from '../storage/factory'
import { Synchronizer } from '../types'

const windowStub = spyOn(window, 'window') as Mock<() => undefined | Window>
Expand All @@ -23,7 +23,7 @@ describe('isLocalStorageAvailable', () => {

describe('storage', () => {
it('should return an object with keys', () => {
const returnedValue = storage()
const returnedValue = storage(1)

expect(returnedValue).toHaveProperty('value')
expect(returnedValue).toHaveProperty('subscribe')
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { MMKV } from 'react-native-mmkv'

export type Synchronizer<T> = {
value: T
subscribe?: (update: (value: T) => void, key: string) => void
Expand Down Expand Up @@ -46,3 +48,9 @@ export type CustomActionsBuilderParams<TState extends object> = {
export type CustomActionsBuilder<TState extends object, TCustomActions extends CustomActions> = (
builderParams: CustomActionsBuilderParams<TState>,
) => TCustomActions

export type CreateStorageOptions = {
mmkvInstance?: MMKV
deserialize?: (value: string) => unknown
serialize?: (value: unknown) => string
}