Properly handle null values coming from JS (#49250)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/49250

The TurboModule System decided to ignore the Null values when they are coming to JS. However, in iOS, null value can be mapped to `[NSNull null];` and this value is a valid value that can be used on the native side.

In the old architecture, when the user were sending a null value from JS to a native module, the Native side was receiving the value.

In the New Architecture, the value was stripped away.

This change allow us to handle the `null` value properly in the interop layer, to restore the usage of legacy modules in the New Arch.

I also tried with a more radical approach, but several tests were crashing because some modules do not know how to handle `NSNull`.

See discussion happening here: https://github.com/invertase/react-native-firebase/issues/8144#issuecomment-2548067344

## Changelog:
[iOS][Changed] - Properly handle `null` values coming from NativeModules.

Reviewed By: sammy-SC

Differential Revision: D69301396

fbshipit-source-id: be275185e2643092f6c3dc2481fe9381bbcf69e9
This commit is contained in:
Riccardo Cipolleschi
2025-02-10 10:14:08 -08:00
committed by Facebook GitHub Bot
parent e74246bd66
commit d4236791e2
3 changed files with 36 additions and 13 deletions
@@ -346,7 +346,7 @@ void ObjCInteropTurboModule::setInvocationArg(
SEL selector = selectorForType(argumentType);
if ([RCTConvert respondsToSelector:selector]) {
id objCArg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_);
id objCArg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_, YES);
if (objCArgType == @encode(char)) {
char arg = RCTConvertTo<char>(selector, objCArg);
@@ -500,7 +500,7 @@ void ObjCInteropTurboModule::setInvocationArg(
}
RCTResponseSenderBlock arg =
(RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_);
(RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_, YES);
if (arg) {
[retainedObjectsForInvocation addObject:arg];
}
@@ -515,7 +515,7 @@ void ObjCInteropTurboModule::setInvocationArg(
}
RCTResponseSenderBlock senderBlock =
(RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_);
(RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_, YES);
RCTResponseErrorBlock arg = ^(NSError *error) {
senderBlock(@[ RCTJSErrorFromNSError(error) ]);
};
@@ -545,7 +545,7 @@ void ObjCInteropTurboModule::setInvocationArg(
runtime, errorPrefix + "JavaScript argument must be a plain object. Got " + getType(runtime, jsiArg));
}
id arg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_);
id arg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_, YES);
RCTManagedPointer *(*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend;
RCTManagedPointer *box = convert([RCTCxxConvert class], selector, arg);
@@ -32,6 +32,11 @@ using EventEmitterCallback = std::function<void(const std::string &, id)>;
namespace TurboModuleConvertUtils {
jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value);
id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, std::shared_ptr<CallInvoker> jsInvoker);
id convertJSIValueToObjCObject(
jsi::Runtime &runtime,
const jsi::Value &value,
std::shared_ptr<CallInvoker> jsInvoker,
BOOL useNSNull);
} // namespace TurboModuleConvertUtils
template <>
@@ -111,21 +111,27 @@ static NSString *convertJSIStringToNSString(jsi::Runtime &runtime, const jsi::St
return [NSString stringWithUTF8String:value.utf8(runtime).c_str()];
}
static NSArray *
convertJSIArrayToNSArray(jsi::Runtime &runtime, const jsi::Array &value, std::shared_ptr<CallInvoker> jsInvoker)
static NSArray *convertJSIArrayToNSArray(
jsi::Runtime &runtime,
const jsi::Array &value,
std::shared_ptr<CallInvoker> jsInvoker,
BOOL useNSNull)
{
size_t size = value.size(runtime);
NSMutableArray *result = [NSMutableArray new];
for (size_t i = 0; i < size; i++) {
// Insert kCFNull when it's `undefined` value to preserve the indices.
id convertedObject = convertJSIValueToObjCObject(runtime, value.getValueAtIndex(runtime, i), jsInvoker);
id convertedObject = convertJSIValueToObjCObject(runtime, value.getValueAtIndex(runtime, i), jsInvoker, useNSNull);
[result addObject:convertedObject ? convertedObject : (id)kCFNull];
}
return [result copy];
}
static NSDictionary *
convertJSIObjectToNSDictionary(jsi::Runtime &runtime, const jsi::Object &value, std::shared_ptr<CallInvoker> jsInvoker)
static NSDictionary *convertJSIObjectToNSDictionary(
jsi::Runtime &runtime,
const jsi::Object &value,
std::shared_ptr<CallInvoker> jsInvoker,
BOOL useNSNull)
{
jsi::Array propertyNames = value.getPropertyNames(runtime);
size_t size = propertyNames.size(runtime);
@@ -133,7 +139,7 @@ convertJSIObjectToNSDictionary(jsi::Runtime &runtime, const jsi::Object &value,
for (size_t i = 0; i < size; i++) {
jsi::String name = propertyNames.getValueAtIndex(runtime, i).getString(runtime);
NSString *k = convertJSIStringToNSString(runtime, name);
id v = convertJSIValueToObjCObject(runtime, value.getProperty(runtime, name), jsInvoker);
id v = convertJSIValueToObjCObject(runtime, value.getProperty(runtime, name), jsInvoker, useNSNull);
if (v) {
result[k] = v;
}
@@ -161,9 +167,21 @@ convertJSIFunctionToCallback(jsi::Runtime &rt, jsi::Function &&function, std::sh
id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, std::shared_ptr<CallInvoker> jsInvoker)
{
if (value.isUndefined() || value.isNull()) {
return convertJSIValueToObjCObject(runtime, value, jsInvoker, NO);
}
id convertJSIValueToObjCObject(
jsi::Runtime &runtime,
const jsi::Value &value,
std::shared_ptr<CallInvoker> jsInvoker,
BOOL useNSNull)
{
if (value.isUndefined() || (value.isNull() && !useNSNull)) {
return nil;
}
if (value.isNull() && useNSNull) {
return [NSNull null];
}
if (value.isBool()) {
return @(value.getBool());
}
@@ -176,12 +194,12 @@ id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, s
if (value.isObject()) {
jsi::Object o = value.getObject(runtime);
if (o.isArray(runtime)) {
return convertJSIArrayToNSArray(runtime, o.getArray(runtime), jsInvoker);
return convertJSIArrayToNSArray(runtime, o.getArray(runtime), jsInvoker, useNSNull);
}
if (o.isFunction(runtime)) {
return convertJSIFunctionToCallback(runtime, o.getFunction(runtime), jsInvoker);
}
return convertJSIObjectToNSDictionary(runtime, o, jsInvoker);
return convertJSIObjectToNSDictionary(runtime, o, jsInvoker, useNSNull);
}
throw std::runtime_error("Unsupported jsi::Value kind");