react-native-mmkv
Version:
The fastest key/value storage for React Native. ~30x faster than AsyncStorage! Works on Android, iOS and Web.
248 lines (227 loc) • 7.11 kB
text/typescript
import { useRef, useState, useMemo, useCallback, useEffect } from 'react';
import { MMKV } from './MMKV';
import type { Configuration } from './Types';
function isConfigurationEqual(
left?: Configuration,
right?: Configuration
): boolean {
if (left == null || right == null) return left == null && right == null;
return (
left.encryptionKey === right.encryptionKey &&
left.id === right.id &&
left.path === right.path &&
left.mode === right.mode
);
}
let defaultInstance: MMKV | null = null;
function getDefaultInstance(): MMKV {
if (defaultInstance == null) {
defaultInstance = new MMKV();
}
return defaultInstance;
}
/**
* Use the default, shared MMKV instance.
*/
export function useMMKV(): MMKV;
/**
* Use a custom MMKV instance with the given configuration.
* @param configuration The configuration to initialize the MMKV instance with. Does not have to be memoized.
*/
export function useMMKV(configuration: Configuration): MMKV;
export function useMMKV(configuration?: Configuration): MMKV {
const instance = useRef<MMKV>();
const lastConfiguration = useRef<Configuration>();
if (configuration == null) return getDefaultInstance();
if (
instance.current == null ||
!isConfigurationEqual(lastConfiguration.current, configuration)
) {
lastConfiguration.current = configuration;
instance.current = new MMKV(configuration);
}
return instance.current;
}
function createMMKVHook<
T extends (boolean | number | string | ArrayBufferLike) | undefined,
TSet extends T | undefined,
TSetAction extends TSet | ((current: T) => TSet),
>(getter: (instance: MMKV, key: string) => T) {
return (
key: string,
instance?: MMKV
): [value: T, setValue: (value: TSetAction) => void] => {
const mmkv = instance ?? getDefaultInstance();
const [bump, setBump] = useState(0);
const value = useMemo(() => {
// bump is here as an additional outside dependency, so this useMemo
// re-computes the value each time bump changes, effectively acting as a hint
// that the outside value (storage) has changed. setting bump refreshes this value.
bump;
return getter(mmkv, key);
}, [mmkv, key, bump]);
// update value by user set
const set = useCallback(
(v: TSetAction) => {
const newValue = typeof v === 'function' ? v(getter(mmkv, key)) : v;
switch (typeof newValue) {
case 'number':
case 'string':
case 'boolean':
mmkv.set(key, newValue);
break;
case 'undefined':
mmkv.delete(key);
break;
case 'object':
if (newValue instanceof ArrayBuffer) {
mmkv.set(key, newValue);
break;
} else {
throw new Error(
`MMKV: Type object (${newValue}) is not supported!`
);
}
default:
throw new Error(`MMKV: Type ${typeof newValue} is not supported!`);
}
},
[key, mmkv]
);
// update value if it changes somewhere else (second hook, same key)
useEffect(() => {
const listener = mmkv.addOnValueChangedListener((changedKey) => {
if (changedKey === key) {
setBump((b) => b + 1);
}
});
return () => listener.remove();
}, [key, mmkv]);
return [value, set];
};
}
/**
* Use the string value of the given `key` from the given MMKV storage instance.
*
* If no instance is provided, a shared default instance will be used.
*
* @example
* ```ts
* const [username, setUsername] = useMMKVString("user.name")
* ```
*/
export const useMMKVString = createMMKVHook((instance, key) =>
instance.getString(key)
);
/**
* Use the number value of the given `key` from the given MMKV storage instance.
*
* If no instance is provided, a shared default instance will be used.
*
* @example
* ```ts
* const [age, setAge] = useMMKVNumber("user.age")
* ```
*/
export const useMMKVNumber = createMMKVHook((instance, key) =>
instance.getNumber(key)
);
/**
* Use the boolean value of the given `key` from the given MMKV storage instance.
*
* If no instance is provided, a shared default instance will be used.
*
* @example
* ```ts
* const [isPremiumAccount, setIsPremiumAccount] = useMMKVBoolean("user.isPremium")
* ```
*/
export const useMMKVBoolean = createMMKVHook((instance, key) =>
instance.getBoolean(key)
);
/**
* Use the buffer value (unsigned 8-bit (0-255)) of the given `key` from the given MMKV storage instance.
*
* If no instance is provided, a shared default instance will be used.
*
* @example
* ```ts
* const [privateKey, setPrivateKey] = useMMKVBuffer("user.privateKey")
* ```
*/
export const useMMKVBuffer = createMMKVHook((instance, key) =>
instance.getBuffer(key)
);
/**
* Use an object value of the given `key` from the given MMKV storage instance.
*
* If no instance is provided, a shared default instance will be used.
*
* The object will be serialized using `JSON`.
*
* @example
* ```ts
* const [user, setUser] = useMMKVObject<User>("user")
* ```
*/
export function useMMKVObject<T>(
key: string,
instance?: MMKV
): [
value: T | undefined,
setValue: (
value: T | undefined | ((prevValue: T | undefined) => T | undefined)
) => void,
] {
const [json, setJson] = useMMKVString(key, instance);
const value = useMemo(() => {
if (json == null) return undefined;
return JSON.parse(json) as T;
}, [json]);
const setValue = useCallback(
(v: (T | undefined) | ((prev: T | undefined) => T | undefined)) => {
if (v instanceof Function) {
setJson((currentJson) => {
const currentValue =
currentJson != null ? (JSON.parse(currentJson) as T) : undefined;
const newValue = v(currentValue);
// Store the Object as a serialized Value or clear the value
return newValue != null ? JSON.stringify(newValue) : undefined;
});
} else {
// Store the Object as a serialized Value or clear the value
const newValue = v != null ? JSON.stringify(v) : undefined;
setJson(newValue);
}
},
[setJson]
);
return [value, setValue];
}
/**
* Listen for changes in the given MMKV storage instance.
* If no instance is passed, the default instance will be used.
* @param valueChangedListener The function to call whenever a value inside the storage instance changes
* @param instance The instance to listen to changes to (or the default instance)
*
* @example
* ```ts
* useMMKVListener((key) => {
* console.log(`Value for "${key}" changed!`)
* })
* ```
*/
export function useMMKVListener(
valueChangedListener: (key: string) => void,
instance?: MMKV
): void {
const ref = useRef(valueChangedListener);
ref.current = valueChangedListener;
const mmkv = instance ?? getDefaultInstance();
useEffect(() => {
const listener = mmkv.addOnValueChangedListener((changedKey) => {
ref.current(changedKey);
});
return () => listener.remove();
}, [mmkv]);
}