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
32 changes: 32 additions & 0 deletions docs/LISTENERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,38 @@ const listener = storage.addOnValueChangedListener((changedKey) => {
})
```

### Listen to native changes from another process.

On native platforms, MMKV can detect changes from another process, such as an app
extension, App Clip, or background service. These changes are routed through
`addOnValueChangedListener(...)` as well.

MMKV does not report the exact changed key for external changes, so React Native
MMKV conservatively notifies listeners for all currently known keys and all
previously known keys. This ensures value hooks and `useMMKVKeys()` refresh for
adds, updates, and deletes.

```ts
const storage = createMMKV({
id: 'shared-storage',
mode: 'multi-process',
})

const listener = storage.addOnValueChangedListener((changedKey) => {
const value = storage.getString(changedKey)
console.log(`"${changedKey}" might have changed: ${value}`)
})
```

You can also force MMKV to check for external changes:

```ts
storage.checkExternalContentChanged()
```

The built-in `useMMKV*` value hooks and `useMMKVKeys()` automatically refresh
when this native event fires.

Don't forget to remove the listener when no longer needed. For example, when the user logs out:

```ts
Expand Down
11 changes: 11 additions & 0 deletions example/__tests__/MMKV.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,17 @@ describe('MMKV Listeners & Observers', () => {
listener.remove();
});
});

describe('External Content Change Checks', () => {
it('should allow manually checking for external content changes', async () => {
storage.checkExternalContentChanged();
storage.set('external-api-test', 'value');

await waitForNextTick();

expect(storage.getString('external-api-test')).toStrictEqual('value');
});
});
});

describe('Deleting instances and checking if they exist', () => {
Expand Down
18 changes: 12 additions & 6 deletions packages/react-native-mmkv/cpp/HybridMMKV.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
//

#include "HybridMMKV.hpp"
#include "MMKVListenerRegistry.hpp"
#include "MMKVTypes.hpp"
#include "MMKVValueChangedListenerRegistry.hpp"
#include "ManagedMMBuffer.hpp"
#include <NitroModules/NitroLogger.hpp>

Expand Down Expand Up @@ -64,6 +64,8 @@ HybridMMKV::HybridMMKV(const Configuration& config) : HybridObject(TAG) {

throw std::runtime_error("Failed to create MMKV instance!");
}

MMKVListenerRegistry::registerInstance(instance);
}

std::string HybridMMKV::getId() {
Expand Down Expand Up @@ -127,7 +129,7 @@ void HybridMMKV::set(const std::string& key, const std::variant<bool, std::share
}

// Notify on changed
MMKVValueChangedListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
MMKVListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
}

std::optional<bool> HybridMMKV::getBoolean(const std::string& key) {
Expand Down Expand Up @@ -178,7 +180,7 @@ bool HybridMMKV::remove(const std::string& key) {
bool wasRemoved = instance->removeValueForKey(key);
if (wasRemoved) {
// Notify on changed
MMKVValueChangedListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
MMKVListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
}
return wasRemoved;
}
Expand All @@ -192,7 +194,7 @@ void HybridMMKV::clearAll() {
instance->clearAll();
for (const auto& key : keysBefore) {
// Notify on changed
MMKVValueChangedListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
MMKVListenerRegistry::notifyOnValueChanged(instance->mmapID(), key);
}
}

Expand Down Expand Up @@ -224,14 +226,18 @@ void HybridMMKV::trim() {
instance->clearMemoryCache();
}

void HybridMMKV::checkExternalContentChanged() {
instance->checkContentChanged();
}

Listener HybridMMKV::addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) {
// Add listener
auto mmkvID = instance->mmapID();
auto listenerID = MMKVValueChangedListenerRegistry::addListener(mmkvID, onValueChanged);
auto listenerID = MMKVListenerRegistry::addValueChangedListener(mmkvID, onValueChanged);

return Listener([=]() {
// remove()
MMKVValueChangedListenerRegistry::removeListener(mmkvID, listenerID);
MMKVListenerRegistry::removeValueChangedListener(mmkvID, listenerID);
});
}

Expand Down
1 change: 1 addition & 0 deletions packages/react-native-mmkv/cpp/HybridMMKV.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class HybridMMKV final : public HybridMMKVSpec {
void encrypt(const std::string& key, std::optional<EncryptionType> encryptionType) override;
void decrypt() override;
void trim() override;
void checkExternalContentChanged() override;
Listener addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) override;
double importAllFrom(const std::shared_ptr<HybridMMKVSpec>& other) override;

Expand Down
5 changes: 4 additions & 1 deletion packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#include "HybridMMKVFactory.hpp"
#include "HybridMMKV.hpp"
#include "MMKVGlobalHandler.hpp"
#include "MMKVListenerRegistry.hpp"
#include "MMKVTypes.hpp"

namespace margelo::nitro::mmkv {
Expand All @@ -19,14 +21,15 @@ void HybridMMKVFactory::initializeMMKV(const std::string& rootPath) {
Logger::log(LogLevel::Info, TAG, "Initializing MMKV with rootPath=%s", rootPath.c_str());

MMKVLogLevel logLevel = static_cast<MMKVLogLevel>(MMKV_LOG_LEVEL);
MMKV::initializeMMKV(rootPath, logLevel);
MMKV::initializeMMKV(rootPath, logLevel, &MMKVGlobalHandler::shared());
}

std::shared_ptr<HybridMMKVSpec> HybridMMKVFactory::createMMKV(const Configuration& configuration) {
return std::make_shared<HybridMMKV>(configuration);
}

bool HybridMMKVFactory::deleteMMKV(const std::string& id) {
MMKVListenerRegistry::unregisterInstance(id);
return MMKV::removeStorage(id);
}

Expand Down
73 changes: 73 additions & 0 deletions packages/react-native-mmkv/cpp/MMKVGlobalHandler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// MMKVGlobalHandler.cpp
// react-native-mmkv
//
// Created by Marc Rousavy on 03.06.2026.
//

#include "MMKVGlobalHandler.hpp"
#include "MMKVListenerRegistry.hpp"
#include <NitroModules/NitroLogger.hpp>

namespace margelo::nitro::mmkv {

namespace {

LogLevel getNitroLogLevel(::mmkv::MMKVLogLevel level) {
switch (level) {
case ::mmkv::MMKVLogDebug:
return LogLevel::Debug;
case ::mmkv::MMKVLogInfo:
return LogLevel::Info;
case ::mmkv::MMKVLogWarning:
return LogLevel::Warning;
case ::mmkv::MMKVLogError:
case ::mmkv::MMKVLogNone:
return LogLevel::Error;
}
return LogLevel::Error;
}

} // namespace

MMKVGlobalHandler& MMKVGlobalHandler::shared() {
static MMKVGlobalHandler handler;
return handler;
}

void MMKVGlobalHandler::mmkvLog(::mmkv::MMKVLogLevel level, const char* file, int line, const char* function, ::mmkv::MMKVLog_t message) {
auto logMessage = getLogMessage(message);
Logger::log(getNitroLogLevel(level), "MMKV", "%s:%d %s: %s", file != nullptr ? file : "", line, function != nullptr ? function : "",
logMessage.c_str());
}

::mmkv::MMKVRecoverStrategic MMKVGlobalHandler::onMMKVCRCCheckFail(const std::string& /* mmapID */) {
return ::mmkv::OnErrorDiscard;
}

::mmkv::MMKVRecoverStrategic MMKVGlobalHandler::onMMKVFileLengthError(const std::string& /* mmapID */) {
return ::mmkv::OnErrorDiscard;
}

void MMKVGlobalHandler::onContentChangedByOuterProcess(const std::string& mmapID) {
MMKVListenerRegistry::notifyOnExternalContentChanged(mmapID);
}

void MMKVGlobalHandler::onMMKVContentLoadSuccessfully(const std::string& /* mmapID */) {
// no-op
}

std::string MMKVGlobalHandler::getLogMessage(::mmkv::MMKVLog_t message) {
#if defined(__ANDROID__)
return message;
#elif defined(__OBJC__)
if (message == nullptr) {
return "";
}
return std::string([message UTF8String]);
#else
return "";
#endif
}

} // namespace margelo::nitro::mmkv
32 changes: 32 additions & 0 deletions packages/react-native-mmkv/cpp/MMKVGlobalHandler.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// MMKVGlobalHandler.hpp
// react-native-mmkv
//
// Created by Marc Rousavy on 03.06.2026.
//

#pragma once

#include "MMKVTypes.hpp"

namespace margelo::nitro::mmkv {

class MMKVGlobalHandler final : public ::mmkv::MMKVHandler {
public:
static MMKVGlobalHandler& shared();

public:
void mmkvLog(::mmkv::MMKVLogLevel level, const char* file, int line, const char* function, ::mmkv::MMKVLog_t message) override;
::mmkv::MMKVRecoverStrategic onMMKVCRCCheckFail(const std::string& mmapID) override;
::mmkv::MMKVRecoverStrategic onMMKVFileLengthError(const std::string& mmapID) override;
void onContentChangedByOuterProcess(const std::string& mmapID) override;
void onMMKVContentLoadSuccessfully(const std::string& mmapID) override;

private:
MMKVGlobalHandler() = default;

private:
static std::string getLogMessage(::mmkv::MMKVLog_t message);
};

} // namespace margelo::nitro::mmkv
Loading
Loading