How do react-native-mmkv's useMMKVString and sibling hooks keep a React Native component in sync when a stored value changes?
answer
- useSyncExternalStore underneath
- subscribe via addOnValueChangedListener
- filtered to the hook's key
- setter with undefined removes the key
- other processes: checked on app active
basics
~20 sThe hooks subscribe to the instance's value-changed listener through useSyncExternalStore and re-read their key synchronously whenever that key changes. Any write to the same instance id in the app, from a hook or plain code, re-renders every component using that key.
solid answer
~40 s`useMMKVString(key, instance?)`, `useMMKVNumber`, `useMMKVBoolean`, `useMMKVBuffer` and `useMMKVObject` return a `[value, setValue]` pair. Under the hood they use React's `useSyncExternalStore`: the subscribe function registers `addOnValueChangedListener` on the instance and only notifies React when the changed key matches, and the snapshot is a synchronous getter call, so the first render already has the value. Because the native listener registry is keyed by instance id, a `storage.set` from a non-React module, another screen or another hook re-renders every subscriber. The setter accepts a value or an updater; passing `undefined` removes the key. `useMMKVObject` wraps the string hook with `JSON.parse`/`JSON.stringify`. Outside components I use `addOnValueChangedListener` directly and call `remove()` on the returned listener when done.
go deeper
Recall the typed hooks, that they return a value and a setter like useState, and that setting undefined removes the key.
Explain the useSyncExternalStore mechanism: a key-filtered value-changed listener for subscription and a synchronous getter for the snapshot.
Reason about who triggers re-renders (any write to the same id, clearAll for every key), listener cleanup, and the cost of large objects behind useMMKVObject.
Decide when MMKV hooks can serve as lightweight shared state and when a dedicated state library should own the data, with MMKV only persisting it.
## The hook family react-native-mmkv ships React hooks for function components: | Hook | Returns | Notes | |---|---|---| | `useMMKVString(key, instance?)` | `[string \| undefined, setter]` | | | `useMMKVNumber(key, instance?)` | `[number \| undefined, setter]` | | | `useMMKVBoolean(key, instance?)` | `[boolean \| undefined, setter]` | | | `useMMKVBuffer(key, instance?)` | `[ArrayBuffer \| undefined, setter]` | | | `useMMKVObject<T>(key, instance?)` | `[T \| undefined, setter]` | JSON over the string hook | | `useMMKV(config?)` | an instance | default or configured instance | | `useMMKVListener(fn, instance?)` | nothing | called with each changed key | | `useMMKVKeys(instance?)` | `string[]` | updates as keys are added or removed | Without the `instance` argument the hooks use a shared default instance. ## How a hook stays in sync The typed hooks are built by one factory around React's **`useSyncExternalStore`**, the hook React provides for subscribing to stores that live outside React: 1. **Subscribe.** The hook calls `mmkv.addOnValueChangedListener(changedKey => ...)` and only tells React to re-render when `changedKey === key`. The returned listener's `remove()` runs on unmount or when the key or instance changes. 2. **Snapshot.** React reads the current value with a synchronous getter such as `mmkv.getString(key)`. Because this read is synchronous, the value is present in the first render, with no loading state. 3. **Set.** The setter accepts a value or an updater function. Strings, numbers, booleans and `ArrayBuffer`s are written with `set`; **`undefined` calls `remove(key)`**; any other object throws, which is why objects go through `useMMKVObject`. ```tsx import { Switch } from "react-native"; import { useMMKVBoolean } from "react-native-mmkv"; export function ShowCompletedToggle() { const [showCompleted = false, setShowCompleted] = useMMKVBoolean("showCompleted"); return <Switch value={showCompleted} onValueChange={setShowCompleted} />; } ``` ## Who triggers a re-render In native code the listener registry is **keyed by the instance id**, and `set`, `remove` and `clearAll` notify it. Consequences: - A write from a plain TypeScript module (a sync service, a sign-out routine) re-renders every mounted component that uses that key. - Two separate `createMMKV({ id: "x" })` objects in the same app share notifications, because they share the id. - `remove` notifies only when a key was actually removed. - `clearAll()` notifies once for every key that existed. Changes made by **another process**, such as an app extension writing to a shared app-group instance, are not pushed through this registry. The library calls `checkContentChanged()` on each instance when the app returns to the `active` state, which reloads data changed outside the process; instances meant for that should use `mode: "multi-process"`. ## Using listeners outside components ```typescript const listener = storage.addOnValueChangedListener((key) => { if (key === "todos") syncBadgeCount(); }); // later, for example on sign-out listener.remove(); ``` A listener that is never removed keeps its callback, and whatever the callback closes over, alive. `useMMKVListener` handles removal for you inside components and always calls the latest callback. ## Watching keys rather than values Two hooks observe the instance rather than one key: - **`useMMKVListener(fn, instance?)`** registers a listener for the component's lifetime and always invokes the latest `fn`, so the callback does not need to be memoised. It suits side effects such as updating a badge when `todos` changes. - **`useMMKVKeys(instance?)`** returns the list of keys and re-fetches it only when a change adds or removes a key; overwriting an existing key does not trigger a re-fetch. It suits screens that list per-item keys, such as `todo-<id>` entries. Both follow the same rule as the typed hooks: they see every in-process write to the instance id, whoever made it. ## Pitfalls - **Objects re-parse on every change.** `useMMKVObject` stores JSON, so each write to that key parses the whole document again and yields a new object identity. A large to-do list stored under one key re-parses on every toggle. - **Corrupt JSON throws during render.** `useMMKVObject` calls `JSON.parse` without a guard, so a bad stored value surfaces as a render error. - **Writing during render** creates a loop: the write notifies, the component re-renders, and writes again. Write in event handlers or effects. - **Missing values are `undefined`.** Supply a default with destructuring, as above, rather than assuming a stored value exists.
- Why does a component using useMMKVString not need a loading state?The hook's snapshot is a synchronous `getString` call, so React receives the stored value during the first render. With Async Storage the value arrives only after a Promise resolves, which forces an initial placeholder render.
- An app extension writes to a shared MMKV instance. When does the main app see it?Not through the in-process listener registry, which only covers writes made inside the app process. The library calls `checkContentChanged()` when the app becomes active, reloading data changed outside the process; such instances should be created with `mode: 'multi-process'`.
saying these in an interview costs you the question
- Believing MMKV hooks only see writes made through the same hook
- Expecting setValue(undefined) to store the string 'undefined'
- Passing a plain object to useMMKVString's setter
- Forgetting to call remove() on a manually added listener
- Writing to a hook's key during render