Skip to content
Open
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
6 changes: 3 additions & 3 deletions packages/react-native-mmkv/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dev-plugins/react-native-mmkv",
"version": "0.4.0",
"version": "0.4.1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"version": "0.4.1",
"version": "0.4.0",

kindly keep it as it was and we'll see how to bump it

"description": "Expo DevTools Plugin for react-native-mmkv",
"main": "build/index.js",
"types": "build/index.d.ts",
Expand Down Expand Up @@ -35,9 +35,9 @@
"license": "MIT",
"devDependencies": {
"@types/react": "~19.0.10",
"expo": "53.0.7",
"expo": "^54.0.0",
"expo-module-scripts": "^4.1.7",
"react-native-mmkv": "^3.1.0",
"react-native-mmkv": "^4.0.0",
"typescript": "~5.8.3"
},
"peerDependencies": {
Expand Down
131 changes: 66 additions & 65 deletions packages/react-native-mmkv/src/useMMKVDevTools.ts
Original file line number Diff line number Diff line change
@@ -1,104 +1,105 @@
import { useDevToolsPluginClient, type EventSubscription } from 'expo/devtools';
import { useCallback, useEffect } from 'react';
import { MMKV } from 'react-native-mmkv';
import { useCallback, useEffect, useMemo } from 'react';
import { createMMKV, type MMKV } from 'react-native-mmkv';

import { Method } from '../methods';

/**
* This hook registers a devtools plugin for react-native-mmkv.
*
* The plugin provides you with the ability to view, add, edit, and remove react-native-mmkv entries.
*
* @param props
* @param props.errorHandler - A function that will be called with any errors that occur while communicating
* with the devtools, if not provided errors will be ignored. Setting this is highly recommended.
* @param props.storage - A MMKV storage instance to use, if not provided the default storage will be used.
*/
export function useMMKVDevTools({
errorHandler,
storage = new MMKV(),
}: {
type Params = {
errorHandler?: (error: Error) => void;
storage?: MMKV;
Copy link
Contributor

@vonovak vonovak Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
storage?: MMKV;
storage: MMKV;

let's make storage required. Then we don't need to worry about how to construct mmkv and the plugin potentially stays compatible with both v3 and v4 (and so on)

} = {}) {
};

export function useMMKVDevTools({
errorHandler,
storage: storageProp,
}: Params = {}) {
const client = useDevToolsPluginClient('mmkv');

// Ensure we create MMKV only once if not provided
const storage = useMemo(() => storageProp ?? createMMKV(), [storageProp]);

const handleError = useCallback(
(error: unknown) => {
if (error instanceof Error) {
errorHandler?.(error);
} else {
errorHandler?.(new Error(`Unknown error: ${String(error)}`));
}
const err = error instanceof Error ? error : new Error(String(error));
errorHandler?.(err);
},
[errorHandler]
);

useEffect(() => {
if (!client) return;

const on = (
event: Method,
listener: (params: { key?: string; value?: string }) => Promise<any>
) =>
client?.addMessageListener(event, async (params: { key?: string; value?: string }) => {
): EventSubscription =>
client.addMessageListener(event, async (params: { key?: string; value?: string }) => {
try {
const result = await listener(params);

client?.sendMessage(`ack:${event}`, { result });
client.sendMessage(`ack:${event}`, { result: result ?? true });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why result ?? true

} catch (error) {
// send a serializable error payload
const err = error instanceof Error ? { message: error.message, stack: error.stack } : { message: String(error) };
try {
client?.sendMessage('error', { error });
client.sendMessage('error', err);
} finally {
handleError(error);
} catch (e) {
handleError(e);
}
}
});

const subscriptions: (EventSubscription | undefined)[] = [];
const subscriptions: EventSubscription[] = [];

try {
subscriptions.push(
on('getAll', async () => {
const keys = storage.getAllKeys();
return keys?.map((key) => [key, storage.getString(key)]);
})
);
} catch (e) {
handleError(e);
}
// getAll
subscriptions.push(
on('getAll', async () => {
const keys = storage.getAllKeys() ?? [];
// Try to read strings; fall back to number/bool if not string
return keys.map((key) => {
const s = storage.getString(key);
if (s !== undefined) return [key, s];

try {
subscriptions.push(
on('set', async ({ key, value }) => {
if (key !== undefined && value !== undefined) {
return storage.set(key, value);
}
})
);
} catch (e) {
handleError(e);
}
const n = storage.getNumber?.(key);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const n = storage.getNumber?.(key);
const n = storage.getNumber(key);

no need for optional here and below?

if (typeof n === 'number' && !Number.isNaN(n)) return [key, String(n)];

try {
subscriptions.push(
on('remove', async ({ key }) => {
if (key !== undefined) {
storage.delete(key);
}
})
);
} catch (e) {
handleError(e);
}
const b = storage.getBoolean?.(key);
if (typeof b === 'boolean') return [key, String(b)];

return [key, undefined];
});
})
);

// set
subscriptions.push(
on('set', async ({ key, value }) => {
if (key !== undefined && value !== undefined) {
storage.set(key, value);
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the reason to return a boolean?

}
return false;
})
);

// remove
subscriptions.push(
on('remove', async ({ key }) => {
if (key !== undefined) {
storage.remove(key);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v3 uses the delete method. By calling delete or remove we could stay compatible with both versions.

If that's a problem for some reason, lets target only v4. in which case we need to bump the mmkv entry in peerDependencies

return true;
}
return false;
})
);

return () => {
for (const subscription of subscriptions) {
for (const sub of subscriptions) {
try {
subscription?.remove();
sub.remove();
} catch (e) {
handleError(e);
}
}
};
}, [client]);
}, [client, storage, handleError]);
}