react-native-mmkv
Version:
The fastest key/value storage for React Native. ~30x faster than AsyncStorage! Works on Android, iOS and Web.
233 lines (213 loc) • 6.46 kB
text/typescript
import { useRef, useState, useMemo, useCallback, useEffect } from 'react';
import { MMKV, MMKVConfiguration } from './MMKV';
function isConfigurationEqual(
left?: MMKVConfiguration,
right?: MMKVConfiguration
): boolean {
if (left == null || right == null) return left == null && right == null;
return (
left.encryptionKey === right.encryptionKey &&
left.id === right.id &&
left.path === right.path
);
}
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: MMKVConfiguration): MMKV;
export function useMMKV(configuration?: MMKVConfiguration): MMKV {
const instance = useRef<MMKV>();
const lastConfiguration = useRef<MMKVConfiguration>();
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 | Uint8Array) | 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 [value, setValue] = useState(() => getter(mmkv, key));
const valueRef = useRef<T>(value);
valueRef.current = value;
// update value by user set
const set = useCallback(
(v: TSetAction) => {
const newValue = typeof v === 'function' ? v(valueRef.current) : 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 Uint8Array) {
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 key or instance changes
useEffect(() => {
setValue(getter(mmkv, key));
}, [key, mmkv]);
// update value if it changes somewhere else (second hook, same key)
useEffect(() => {
const listener = mmkv.addOnValueChangedListener((changedKey) => {
if (changedKey === key) {
setValue(getter(mmkv, key));
}
});
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) => void] {
const [string, setString] = useMMKVString(key, instance);
const value = useMemo(() => {
if (string == null) return undefined;
return JSON.parse(string) as T;
}, [string]);
const setValue = useCallback(
(v: T | undefined) => {
if (v == null) {
// Clear the Value
setString(undefined);
} else {
// Store the Object as a serialized Value
setString(JSON.stringify(v));
}
},
[setString]
);
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]);
}