├── .github └── img │ ├── flipper.png │ └── install.png ├── .gitignore ├── README.md ├── lerna.json ├── package.json ├── packages ├── flipper-plugin-react-native-mmkv │ ├── .gitignore │ ├── babel.config.js │ ├── package.json │ ├── src │ │ └── index.tsx │ └── tsconfig.json └── react-native-mmkv-flipper-plugin │ ├── .gitignore │ ├── index.ts │ ├── package.json │ └── tsconfig.json └── yarn.lock /.github/img/flipper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muchobien/flipper-plugin-react-native-mmkv/c5d0cf586f6d1ce2233fe93fba169c219c8719d9/.github/img/flipper.png -------------------------------------------------------------------------------- /.github/img/install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muchobien/flipper-plugin-react-native-mmkv/c5d0cf586f6d1ce2233fe93fba169c219c8719d9/.github/img/install.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flipper plugin for [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) 2 | 3 | This plugin allows you to control react-native-mmkv from flipper 4 | 5 | ## Usage 6 | 7 | ### Install the flipper plugin. 8 | 9 | Simply search for 'react-native-mmkv' in the `Plugin Manager` in Flipper. 10 | 11 |

12 | install plugin 13 |

14 | 15 | ### In your react-native App 16 | 17 | ```sh 18 | yarn add -D react-native-flipper 19 | yarn add react-native-mmkv-flipper-plugin 20 | ``` 21 | 22 | ```ts 23 | import { MMKV } from "react-native-mmkv"; 24 | import { initializeMMKVFlipper } from "react-native-mmkv-flipper-plugin"; 25 | const storage = new MMKV(); 26 | 27 | // add this line inside your App.tsx 28 | if (__DEV__) { 29 | initializeMMKVFlipper({ default: storage }); 30 | } 31 | ``` 32 | 33 | ## Screenshot 34 | 35 | ![view](./.github/img/flipper.png) 36 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "1.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flipper-plugin-react-native-mmkv", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "private": true, 7 | "workspaces": { 8 | "packages": [ 9 | "packages/*" 10 | ] 11 | }, 12 | "devDependencies": { 13 | "lerna": "^4.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/flipper-plugin-react-native-mmkv/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | -------------------------------------------------------------------------------- /packages/flipper-plugin-react-native-mmkv/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-typescript', 4 | '@babel/preset-react', 5 | ['@babel/preset-env', {targets: {node: 'current'}}] 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/flipper-plugin-react-native-mmkv/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://fbflipper.com/schemas/plugin-package/v2.json", 3 | "name": "flipper-plugin-react-native-mmkv", 4 | "id": "rn-mmkv", 5 | "version": "1.0.0", 6 | "pluginType": "client", 7 | "main": "dist/bundle.js", 8 | "flipperBundlerEntry": "src/index.tsx", 9 | "license": "MIT", 10 | "description": "Flipper plugin for React Native MMKV", 11 | "keywords": [ 12 | "flipper-plugin" 13 | ], 14 | "icon": "apps", 15 | "title": "MMKV", 16 | "scripts": { 17 | "lint": "flipper-pkg lint", 18 | "prepack": "flipper-pkg lint && flipper-pkg bundle", 19 | "build": "flipper-pkg bundle", 20 | "watch": "flipper-pkg bundle --watch", 21 | "test": "jest --no-watchman" 22 | }, 23 | "peerDependencies": { 24 | "@emotion/styled": "latest", 25 | "antd": "latest", 26 | "flipper-plugin": "^0.129.0", 27 | "react": "latest", 28 | "react-dom": "latest" 29 | }, 30 | "devDependencies": { 31 | "@babel/preset-react": "latest", 32 | "@babel/preset-typescript": "latest", 33 | "@emotion/styled": "latest", 34 | "@testing-library/react": "latest", 35 | "@types/jest": "latest", 36 | "@types/react": "latest", 37 | "@types/react-dom": "latest", 38 | "antd": "latest", 39 | "flipper-pkg": "latest", 40 | "flipper-plugin": "latest", 41 | "jest": "latest", 42 | "jest-mock-console": "latest", 43 | "react": "latest", 44 | "react-dom": "latest", 45 | "typescript": "latest" 46 | }, 47 | "jest": { 48 | "testEnvironment": "jsdom" 49 | }, 50 | "dependencies": { 51 | "@ant-design/icons": "4.7.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/flipper-plugin-react-native-mmkv/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo } from "react"; 2 | import { 3 | createDataSource, 4 | createState, 5 | DataInspector, 6 | DataTable, 7 | DataTableColumn, 8 | DetailSidebar, 9 | Layout, 10 | Panel, 11 | PluginClient, 12 | usePlugin, 13 | useValue, 14 | } from "flipper-plugin"; 15 | import { Button, Form, Input, Menu, Select } from "antd"; 16 | import { DeleteOutlined, PlusOutlined } from "@ant-design/icons"; 17 | 18 | type Data = Record; 19 | 20 | type Events = { 21 | "mmkv-remove-key": { key: string; instance: string }; 22 | "mmkv-data": Record; 23 | "mmkv-key": { key: string; value: string | undefined; instance: string }; 24 | }; 25 | 26 | type SetParams = { 27 | key: string; 28 | value: string; 29 | }; 30 | 31 | type Methods = { 32 | "mmkv-remove-key": (params: { 33 | key: string; 34 | instance: string; 35 | }) => Promise; 36 | "mmkv-remove-all": (instance: string) => Promise; 37 | "mmkv-set": (params: { 38 | key: string; 39 | value: string; 40 | instance: string; 41 | }) => Promise; 42 | }; 43 | 44 | type InstanceDict = Record; 45 | 46 | type Row = { 47 | key: string; 48 | value: string; 49 | }; 50 | 51 | // Read more: https://fbflipper.com/docs/tutorial/js-custom#creating-a-first-plugin 52 | // API: https://fbflipper.com/docs/extending/flipper-plugin#pluginclient 53 | export function plugin(client: PluginClient) { 54 | const rows = createDataSource([], { 55 | persist: "rows", 56 | key: "key", 57 | }); 58 | const instances = createState({}, { persist: "instances" }); 59 | const selectedInstance = createState(undefined, { 60 | persist: "selectedInstance", 61 | }); 62 | 63 | const selectedRow = createState(undefined, { 64 | persist: "selection", 65 | }); 66 | 67 | client.onMessage("mmkv-data", (newData) => { 68 | Object.entries(newData).forEach(([name, instance], index) => { 69 | if (index === 0) { 70 | selectedInstance.update(() => name); 71 | } 72 | instances.update((draft) => { 73 | draft[name] = instance; 74 | }); 75 | }); 76 | }); 77 | 78 | client.onMessage("mmkv-key", ({ key, value, instance }) => { 79 | const selected = selectedInstance.get(); 80 | if (value) { 81 | instances.update((draft) => { 82 | if (selected === instance) { 83 | rows.upsert({ key, value }); 84 | } 85 | draft[instance][key] = value; 86 | }); 87 | } else { 88 | instances.update((draft) => { 89 | if (selected === instance) { 90 | rows.deleteByKey(key); 91 | } 92 | delete draft[instance][key]; 93 | }); 94 | } 95 | }); 96 | 97 | const removeKey = (key: string) => { 98 | const instance = selectedInstance.get(); 99 | if (instance) { 100 | selectedRow.set(undefined); 101 | instances.update((draft) => { 102 | rows.deleteByKey(key); 103 | delete draft[instance][key]; 104 | }); 105 | client.send("mmkv-remove-key", { key, instance }); 106 | } 107 | }; 108 | 109 | const removeAll = () => { 110 | const instance = selectedInstance.get(); 111 | if (instance) { 112 | client.send("mmkv-remove-all", instance); 113 | instances.update((draft) => { 114 | delete draft[instance]; 115 | selectedRow.set(undefined); 116 | rows.clear(); 117 | selectedInstance.update(() => undefined); 118 | }); 119 | } 120 | }; 121 | 122 | const set = (params: SetParams) => { 123 | const instance = selectedInstance.get(); 124 | if (instance) { 125 | client.send("mmkv-set", { ...params, instance }); 126 | instances.update((draft) => { 127 | draft[instance][params.key] = params.value; 128 | }); 129 | selectedRow.set(params); 130 | rows.upsert(params); 131 | } 132 | }; 133 | 134 | const updateRows = (update: Row[]) => { 135 | rows.clear(); 136 | update.forEach((row) => { 137 | rows.upsert(row); 138 | }); 139 | }; 140 | 141 | console.log("instances", Object.keys(instances.get())); 142 | 143 | return { 144 | instances, 145 | removeAll, 146 | removeKey, 147 | rows, 148 | selectedInstance, 149 | selectedRow, 150 | set, 151 | updateRows, 152 | }; 153 | } 154 | 155 | const columns: DataTableColumn[] = [ 156 | { 157 | key: "key", 158 | title: "Key", 159 | width: "10%", 160 | }, 161 | { 162 | key: "value", 163 | title: "Value", 164 | }, 165 | ]; 166 | 167 | const safeJSONParse = (value: string) => { 168 | try { 169 | return JSON.parse(value); 170 | } catch { 171 | return value; 172 | } 173 | }; 174 | 175 | function Sidebar({ 176 | instances, 177 | record, 178 | onFinish, 179 | onInstanceChange, 180 | selectedInstance, 181 | }: { 182 | record?: T; 183 | instances: InstanceDict; 184 | onFinish: (params: T) => void; 185 | onInstanceChange: (instance: string) => void; 186 | selectedInstance: string | undefined; 187 | }) { 188 | return ( 189 | 190 | 191 | 208 | 209 | {record && ( 210 | 211 | 218 | 219 | )} 220 | 221 |
228 | 241 | 251 | 252 | 253 | 254 | 257 | 258 |
259 |
260 |
261 | ); 262 | } 263 | 264 | export function Component() { 265 | const instance = usePlugin(plugin); 266 | const selectedInstance = useValue(instance.selectedInstance); 267 | const selectedRecord = useValue(instance.selectedRow); 268 | 269 | const handleSelect = useCallback( 270 | (row: Row | undefined) => { 271 | instance.selectedRow.set(row); 272 | }, 273 | [instance.selectedRow] 274 | ); 275 | 276 | const handleInstanceChange = useCallback((ins: string) => { 277 | instance.selectedInstance.update(() => ins); 278 | }, []); 279 | 280 | useEffect(() => { 281 | const current = instance.instances.get(); 282 | if (current && selectedInstance) { 283 | instance.updateRows( 284 | Object.entries(current[selectedInstance]).map(([key, value]) => ({ 285 | key, 286 | value, 287 | })) 288 | ); 289 | } 290 | }, [selectedInstance]); 291 | 292 | return ( 293 | 294 | 295 | 301 | 307 | 310 | 311 | } 312 | onContextMenu={(record) => ( 313 | } 316 | onClick={() => { 317 | if (!record) return; 318 | instance.removeKey(record.key); 319 | }} 320 | > 321 | Remove 322 | 323 | )} 324 | /> 325 | 332 | 333 | 334 | ); 335 | } 336 | -------------------------------------------------------------------------------- /packages/flipper-plugin-react-native-mmkv/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "ES6", 5 | "jsx": "react", 6 | "sourceMap": true, 7 | "noEmit": true, 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "files": ["src/index.tsx"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/react-native-mmkv-flipper-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /packages/react-native-mmkv-flipper-plugin/index.ts: -------------------------------------------------------------------------------- 1 | import type { Flipper } from "react-native-flipper"; 2 | import type { MMKV } from "react-native-mmkv"; 3 | let FlipperModule: typeof import("react-native-flipper") | undefined; 4 | 5 | try { 6 | FlipperModule = require("react-native-flipper"); 7 | } catch { 8 | // noop 9 | } 10 | 11 | type MMKVInstances = Record; 12 | 13 | type InstanceEntry = { 14 | key: string; 15 | value: string; 16 | instance: string; 17 | }; 18 | 19 | let currentConnection: Flipper.FlipperConnection | null = null; 20 | 21 | export const initializeMMKVFlipper = (instances: MMKVInstances) => { 22 | if (FlipperModule == null) { 23 | throw new Error( 24 | "Please install the 'react-native-flipper' package in your project to use Flipper integration for 'react-native-mmkv'" 25 | ); 26 | } 27 | 28 | const { addPlugin } = FlipperModule; 29 | 30 | if (currentConnection === null) { 31 | addPlugin({ 32 | getId: () => "rn-mmkv", 33 | onConnect(connection) { 34 | currentConnection = connection; 35 | const data = Object.entries(instances).reduce< 36 | Record> 37 | >( 38 | (instancesDict, [name, instance]) => ({ 39 | ...instancesDict, 40 | [name]: instance 41 | .getAllKeys() 42 | .reduce>( 43 | (instanceDict, key) => ({ 44 | ...instanceDict, 45 | [key]: instance.getString(key) ?? null, 46 | }), 47 | {} 48 | ), 49 | }), 50 | {} 51 | ); 52 | 53 | connection.send("mmkv-data", data); 54 | 55 | Object.entries(instances).forEach(([name, instance]) => { 56 | instance.addOnValueChangedListener((key) => { 57 | currentConnection?.send("mmkv-key", { 58 | key, 59 | value: instance.getString(key), 60 | instance: name, 61 | }); 62 | }); 63 | }); 64 | 65 | connection.receive( 66 | "mmkv-remove-key", 67 | ({ key, instance }: Omit) => { 68 | instances[instance].delete(key); 69 | } 70 | ); 71 | 72 | connection.receive( 73 | "mmkv-set", 74 | ({ key, value, instance }: InstanceEntry) => { 75 | instances[instance].set(key, value); 76 | } 77 | ); 78 | 79 | connection.receive("mmkv-remove-all", (instance: string) => { 80 | instances[instance].clearAll(); 81 | }); 82 | }, 83 | onDisconnect() { 84 | currentConnection = null; 85 | }, 86 | runInBackground: () => false, 87 | }); 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /packages/react-native-mmkv-flipper-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-mmkv-flipper-plugin", 3 | "version": "1.0.0", 4 | "description": "React Native's MMKV debugger for Flipper", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "license": "MIT", 8 | "scripts": { 9 | "prepack": "yarn tsc" 10 | }, 11 | "peerDependencies": { 12 | "react-native-flipper": ">=0.99.0", 13 | "react-native-mmkv": ">=1.4.0" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "17.0.10", 17 | "react-native-flipper": "0.129.0", 18 | "react-native-mmkv": "2.0.0", 19 | "typescript": "4.5.5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/react-native-mmkv-flipper-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": ["esnext", "dom"], 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "outDir": "dist", 13 | } 14 | } 15 | --------------------------------------------------------------------------------