nitrogen 0.36.5 → 0.37.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -45,6 +45,15 @@ function getFunctionCallSignature(func) {
45
45
  }
46
46
  return callSignature;
47
47
  }
48
+ /**
49
+ * Whether the given type is optional (`T | undefined`) or not.
50
+ *
51
+ * Note that `null` is a separate type in Nitro (`NullType`) - only `undefined`
52
+ * (or a `?`) makes a type optional.
53
+ */
54
+ function isOptionalType(type) {
55
+ return type.getUnionTypes().some((t) => t.isUndefined());
56
+ }
48
57
  function removeDuplicates(types) {
49
58
  return types.filter((t1, index, array) => {
50
59
  const firstIndexOfType = array.findIndex((t2) => t1.getCode('c++') === t2.getCode('c++'));
@@ -185,7 +194,7 @@ export function createType(language, type, isOptional, typeNode) {
185
194
  }
186
195
  else if (type.isArray()) {
187
196
  const arrayElementType = type.getArrayElementTypeOrThrow();
188
- const elementType = createType(language, arrayElementType, false);
197
+ const elementType = createType(language, arrayElementType, isOptionalType(arrayElementType));
189
198
  return new ArrayType(elementType);
190
199
  }
191
200
  else if (type.isTuple()) {
@@ -198,10 +207,7 @@ export function createType(language, type, isOptional, typeNode) {
198
207
  // It's a function!
199
208
  const callSignature = getFunctionCallSignature(type);
200
209
  const funcReturnType = callSignature.getReturnType();
201
- const isReturnOptional = funcReturnType
202
- .getUnionTypes()
203
- .some((t) => t.isUndefined());
204
- const returnType = createType(language, funcReturnType, isReturnOptional);
210
+ const returnType = createType(language, funcReturnType, isOptionalType(funcReturnType));
205
211
  const parameters = callSignature.getParameters().map((p) => {
206
212
  const declaration = p.getValueDeclarationOrThrow();
207
213
  const parameterType = p.getTypeAtLocation(declaration);
@@ -214,17 +220,14 @@ export function createType(language, type, isOptional, typeNode) {
214
220
  else if (isPromise(type)) {
215
221
  // It's a Promise!
216
222
  const [promiseResolvingType] = getArguments(type, 'Promise', 1);
217
- const isResolvingOptional = promiseResolvingType
218
- .getUnionTypes()
219
- .some((t) => t.isUndefined());
220
- const resolvingType = createType(language, promiseResolvingType, isResolvingOptional);
223
+ const resolvingType = createType(language, promiseResolvingType, isOptionalType(promiseResolvingType));
221
224
  return new PromiseType(resolvingType);
222
225
  }
223
226
  else if (isRecord(type)) {
224
227
  // Record<K, V> -> unordered_map<K, V>
225
228
  const [keyTypeT, valueTypeT] = getArguments(type, 'Record', 2);
226
229
  const keyType = createType(language, keyTypeT, false);
227
- const valueType = createType(language, valueTypeT, false);
230
+ const valueType = createType(language, valueTypeT, isOptionalType(valueTypeT));
228
231
  return new RecordType(keyType, valueType);
229
232
  }
230
233
  else if (isArrayBuffer(type)) {
@@ -440,6 +440,13 @@ export class KotlinCxxBridgedType {
440
440
  }
441
441
  dereferenceToJObject(parameterName) {
442
442
  switch (this.type.kind) {
443
+ // an optional bridges to a (possibly `nullptr`) ref of its wrapping type,
444
+ // so it needs to be dereferenced the same way as the type it wraps.
445
+ case 'optional': {
446
+ const optional = getTypeAs(this.type, OptionalType);
447
+ const bridge = new KotlinCxxBridgedType(optional.wrappingType);
448
+ return bridge.dereferenceToJObject(parameterName);
449
+ }
443
450
  // any jni::HybridClass needs to be dereferenced to jobject with .get()
444
451
  case 'array-buffer':
445
452
  case 'function':
@@ -1,5 +1,5 @@
1
1
  import { createIndentation, indent } from '../utils.js';
2
- import { createFileMetadataString, escapeCppName, isFunction, isNotDuplicate, } from '../syntax/helpers.js';
2
+ import { createFileMetadataString, escapeCppName, isNotDuplicate, } from '../syntax/helpers.js';
3
3
  import { getHybridObjectName } from '../syntax/getHybridObjectName.js';
4
4
  import { includeHeader } from '../syntax/c++/includeNitroHeader.js';
5
5
  import { createHostComponentJs } from './createHostComponentJs.js';
@@ -36,8 +36,16 @@ export function createViewComponentShadowNodeFiles(spec) {
36
36
  const { propsClassName, stateClassName, nameVariable, shadowNodeClassName, descriptorClassName, component, } = getViewComponentNames(spec);
37
37
  const namespace = spec.config.getCxxNamespace('c++', 'views');
38
38
  const props = [...spec.properties, getHybridRefProperty(spec)];
39
- const properties = props.map((p) => `CachedProp<${p.type.getCode('c++')}> ${escapeCppName(p.name)};`);
40
- const cases = props.map((p) => `case hashString("${p.name}"): return true;`);
39
+ const properties = props.map((p) => `nitro::CachedProp<${p.type.getCode('c++')}> ${escapeCppName(p.name)};`);
40
+ const filterCases = props.map((prop) => `case hashString("${prop.name}"): return true;`);
41
+ const comparisons = props.map((prop) => {
42
+ const name = escapeCppName(prop.name);
43
+ return `${name}.hasSameValue(other.${name})`;
44
+ });
45
+ const providedChecks = props.map((prop) => {
46
+ const name = escapeCppName(prop.name);
47
+ return `${name}.isProvided()`;
48
+ });
41
49
  const includes = props
42
50
  .flatMap((p) => p.getRequiredImports('c++').map((i) => includeHeader(i, true)))
43
51
  .filter(isNotDuplicate);
@@ -48,14 +56,15 @@ ${createFileMetadataString(`${component}.hpp`)}
48
56
 
49
57
  #pragma once
50
58
 
51
- #include <optional>
52
- #include <NitroModules/NitroDefines.hpp>
53
- #include <NitroModules/NitroHash.hpp>
54
59
  #include <NitroModules/CachedProp.hpp>
55
- #include <react/renderer/core/ConcreteComponentDescriptor.h>
56
- #include <react/renderer/core/PropsParserContext.h>
60
+ #include <NitroModules/ViewComponentDescriptor.hpp>
61
+ #include <NitroModules/ViewPropsHolderState.hpp>
57
62
  #include <react/renderer/components/view/ConcreteViewShadowNode.h>
58
63
  #include <react/renderer/components/view/ViewProps.h>
64
+ #include <react/renderer/core/PropsParserContext.h>
65
+ #include <react/renderer/core/RawProps.h>
66
+
67
+ #include <string>
59
68
 
60
69
  ${includes.join('\n')}
61
70
 
@@ -81,6 +90,16 @@ namespace ${namespace} {
81
90
  public:
82
91
  ${indent(properties.join('\n'), ' ')}
83
92
 
93
+ [[nodiscard]]
94
+ bool hasSameProps(const ${propsClassName}& other) const noexcept {
95
+ return ${comparisons.join(' &&\n ')};
96
+ }
97
+
98
+ [[nodiscard]]
99
+ bool hasAnyProvidedProps() const noexcept {
100
+ return ${providedChecks.join(' ||\n ')};
101
+ }
102
+
84
103
  private:
85
104
  static bool filterObjectKeys(const std::string& propName);
86
105
  };
@@ -88,32 +107,7 @@ namespace ${namespace} {
88
107
  /**
89
108
  * State for the "${spec.name}" View.
90
109
  */
91
- class ${stateClassName} final {
92
- public:
93
- ${stateClassName}() = default;
94
- explicit ${stateClassName}(const std::shared_ptr<${propsClassName}>& props):
95
- _props(props) {}
96
-
97
- public:
98
- [[nodiscard]]
99
- const std::shared_ptr<${propsClassName}>& getProps() const {
100
- return _props;
101
- }
102
-
103
- public:
104
- #ifdef ANDROID
105
- ${stateClassName}(const ${stateClassName}& /* previousState */, folly::dynamic /* data */) {}
106
- folly::dynamic getDynamic() const {
107
- throw std::runtime_error("${stateClassName} does not support folly!");
108
- }
109
- react::MapBuffer getMapBuffer() const {
110
- throw std::runtime_error("${stateClassName} does not support MapBuffer!");
111
- };
112
- #endif
113
-
114
- private:
115
- std::shared_ptr<${propsClassName}> _props;
116
- };
110
+ using ${stateClassName} = nitro::ViewPropsHolderState<${propsClassName}>;
117
111
 
118
112
  /**
119
113
  * The Shadow Node for the "${spec.name}" View.
@@ -126,74 +120,33 @@ namespace ${namespace} {
126
120
  /**
127
121
  * The Component Descriptor for the "${spec.name}" View.
128
122
  */
129
- class ${descriptorClassName} final: public react::ConcreteComponentDescriptor<${shadowNodeClassName}> {
130
- public:
131
- explicit ${descriptorClassName}(const react::ComponentDescriptorParameters& parameters);
132
-
133
- public:
134
- /**
135
- * A faster path for cloning props - reuses the caching logic from \`${propsClassName}\`.
136
- */
137
- std::shared_ptr<const react::Props> cloneProps(const react::PropsParserContext& context,
138
- const std::shared_ptr<const react::Props>& props,
139
- react::RawProps rawProps) const override;
140
- #ifdef ANDROID
141
- void adopt(react::ShadowNode& shadowNode) const override;
142
- #endif
143
- };
123
+ using ${descriptorClassName} = nitro::ViewComponentDescriptor<${shadowNodeClassName}>;
144
124
 
145
125
  /* The actual view for "${spec.name}" needs to be implemented in platform-specific code. */
146
126
 
147
127
  } // namespace ${namespace}
148
128
  `.trim();
149
- // .cpp code
150
129
  const propInitializers = [
151
130
  'react::ViewProps(context, sourceProps, rawProps, filterObjectKeys)',
131
+ ...props.map((prop) => {
132
+ const name = escapeCppName(prop.name);
133
+ const type = prop.type.getCode('c++');
134
+ return `${name}(nitro::CachedProp<${type}>::fromRawValue("${spec.name}", "${prop.name}", rawProps, sourceProps.${name}))`;
135
+ }),
152
136
  ];
153
- const propCopyInitializers = ['react::ViewProps()'];
154
- for (const prop of props) {
155
- const name = escapeCppName(prop.name);
156
- const type = prop.type.getCode('c++');
157
- let valueConversion = `value`;
158
- if (isFunction(prop.type)) {
159
- // Due to a React limitation, functions cannot be passed to native directly,
160
- // because RN converts them to booleans (`true`). Nitro knows this and just
161
- // wraps functions as objects - the original function is stored in `f`.
162
- valueConversion = `value.asObject(*runtime).getProperty(*runtime, PropNameIDCache::get(*runtime, "f"))`;
163
- }
164
- propInitializers.push(`
165
- ${name}([&]() -> CachedProp<${type}> {
166
- try {
167
- const react::RawValue* rawValue = rawProps.at("${prop.name}", nullptr, nullptr);
168
- if (rawValue == nullptr) return sourceProps.${name};
169
- const auto& [runtime, value] = (std::pair<jsi::Runtime*, jsi::Value>)*rawValue;
170
- return CachedProp<${type}>::fromRawValue(*runtime, ${valueConversion}, sourceProps.${name});
171
- } catch (const std::exception& exc) {
172
- throw std::runtime_error(std::string("${spec.name}.${prop.name}: ") + exc.what());
173
- }
174
- }())`.trim());
175
- propCopyInitializers.push(`${name}(other.${name})`);
176
- }
177
137
  const ctorIndent = createIndentation(propsClassName.length * 2);
178
- const descriptorIndent = createIndentation(descriptorClassName.length);
179
138
  const componentCode = `
180
139
  ${createFileMetadataString(`${component}.cpp`)}
181
140
 
182
141
  #include "${component}.hpp"
183
142
 
184
- #include <string>
185
- #include <exception>
186
- #include <utility>
187
- #include <NitroModules/NitroDefines.hpp>
188
- #include <NitroModules/JSIConverter.hpp>
189
- #include <NitroModules/PropNameIDCache.hpp>
190
- #include <react/renderer/core/RawValue.h>
191
- #include <react/renderer/core/ShadowNode.h>
192
- #include <react/renderer/core/ComponentDescriptor.h>
193
- #include <react/renderer/components/view/ViewProps.h>
143
+ #include <NitroModules/NitroHash.hpp>
144
+ #include <NitroModules/CachedProp.hpp>
194
145
 
195
146
  namespace ${namespace} {
196
147
 
148
+ using namespace facebook;
149
+
197
150
  extern const char ${nameVariable}[] = "${T}";
198
151
 
199
152
  ${propsClassName}::${propsClassName}(const react::PropsParserContext& context,
@@ -203,36 +156,11 @@ namespace ${namespace} {
203
156
 
204
157
  bool ${propsClassName}::filterObjectKeys(const std::string& propName) {
205
158
  switch (hashString(propName)) {
206
- ${indent(cases.join('\n'), ' ')}
159
+ ${indent(filterCases.join('\n'), ' ')}
207
160
  default: return false;
208
161
  }
209
162
  }
210
163
 
211
- ${descriptorClassName}::${descriptorClassName}(const react::ComponentDescriptorParameters& parameters)
212
- : ConcreteComponentDescriptor(parameters,
213
- react::RawPropsParser()) {}
214
-
215
- std::shared_ptr<const react::Props> ${descriptorClassName}::cloneProps(const react::PropsParserContext& context,
216
- ${descriptorIndent} const std::shared_ptr<const react::Props>& props,
217
- ${descriptorIndent} react::RawProps rawProps) const {
218
- // 1. Prepare raw props parser
219
- rawProps.parse(rawPropsParser_);
220
- // 2. Copy props with Nitro's cached copy constructor
221
- return ${shadowNodeClassName}::Props(context, /* & */ rawProps, props);
222
- }
223
-
224
- #ifdef ANDROID
225
- void ${descriptorClassName}::adopt(react::ShadowNode& shadowNode) const {
226
- // This is called immediately after \`ShadowNode\` is created, cloned or in progress.
227
- // On Android, we need to wrap props in our state, which gets routed through Java and later unwrapped in JNI/C++.
228
- auto& concreteShadowNode = static_cast<${shadowNodeClassName}&>(shadowNode);
229
- const std::shared_ptr<const ${propsClassName}>& constProps = concreteShadowNode.getConcreteSharedProps();
230
- const std::shared_ptr<${propsClassName}>& props = std::const_pointer_cast<${propsClassName}>(constProps);
231
- ${stateClassName} state{props};
232
- concreteShadowNode.setStateData(std::move(state));
233
- }
234
- #endif
235
-
236
164
  } // namespace ${namespace}
237
165
  `.trim();
238
166
  const files = [
@@ -34,6 +34,14 @@ import ${javaNamespace}.*
34
34
  * Represents the React Native \`ViewManager\` for the "${spec.name}" Nitro HybridView.
35
35
  */
36
36
  public class ${manager}: SimpleViewManager<View>() {
37
+ /**
38
+ * Represents the View and its last state snapshot (mutable)
39
+ */
40
+ private class HybridViewHolder(
41
+ val hybridView: ${viewImplementation},
42
+ var lastState: StateWrapper? = null,
43
+ )
44
+
37
45
  init {
38
46
  if (RecyclableView::class.java.isAssignableFrom(${viewImplementation}::class.java)) {
39
47
  // Enable view recycling
@@ -48,33 +56,41 @@ public class ${manager}: SimpleViewManager<View>() {
48
56
  override fun createViewInstance(reactContext: ThemedReactContext): View {
49
57
  val hybridView = ${viewImplementation}(reactContext)
50
58
  val view = hybridView.view
51
- view.setTag(associated_hybrid_view_tag, hybridView)
59
+ view.setTag(associated_hybrid_view_tag, HybridViewHolder(hybridView))
52
60
  return view
53
61
  }
54
62
 
55
63
  override fun updateState(view: View, props: ReactStylesDiffMap, stateWrapper: StateWrapper): Any? {
56
- val hybridView = getHybridView(view)
64
+ val holder = getHybridViewHolder(view)
57
65
  ?: throw Error("Couldn't find view $view in local views table!")
66
+ val hybridView = holder.hybridView
67
+ val oldState = holder.lastState
68
+ val newState = stateWrapper
58
69
 
59
70
  // 1. Update each prop individually
60
71
  hybridView.beforeUpdate()
61
- ${stateUpdaterName}.updateViewProps(hybridView, stateWrapper)
72
+ ${stateUpdaterName}.updateViewProps(hybridView, newState, oldState)
62
73
  hybridView.afterUpdate()
74
+ holder.lastState = newState
63
75
 
64
76
  // 2. Continue in base View props
65
- return super.updateState(view, props, stateWrapper)
77
+ return super.updateState(view, props, newState)
66
78
  }
67
79
 
68
80
  override fun onDropViewInstance(view: View) {
69
- val hybridView = getHybridView(view)
70
- hybridView?.onDropView()
81
+ val holder = getHybridViewHolder(view)
82
+ holder?.lastState = null
83
+ holder?.hybridView?.onDropView()
71
84
  return super.onDropViewInstance(view)
72
85
  }
73
86
 
74
87
  protected override fun prepareToRecycleView(reactContext: ThemedReactContext, view: View): View? {
75
- super.prepareToRecycleView(reactContext, view)
76
- val hybridView = getHybridView(view)
88
+ val preparedView = super.prepareToRecycleView(reactContext, view)
89
+ ?: return null
90
+ val holder = getHybridViewHolder(preparedView)
77
91
  ?: return null
92
+ val hybridView = holder.hybridView
93
+ holder.lastState = null
78
94
 
79
95
  @Suppress("USELESS_IS_CHECK")
80
96
  if (hybridView is RecyclableView) {
@@ -88,8 +104,8 @@ public class ${manager}: SimpleViewManager<View>() {
88
104
  }
89
105
  }
90
106
 
91
- private fun getHybridView(view: View): ${viewImplementation}? {
92
- return view.getTag(associated_hybrid_view_tag) as? ${viewImplementation}
107
+ private fun getHybridViewHolder(view: View): HybridViewHolder? {
108
+ return view.getTag(associated_hybrid_view_tag) as? HybridViewHolder
93
109
  }
94
110
  }
95
111
  `.trim();
@@ -105,11 +121,11 @@ internal class ${stateUpdaterName} {
105
121
  companion object {
106
122
  /**
107
123
  * Updates the props for [view] through C++.
108
- * The [state] prop is expected to contain [view]'s props as wrapped Fabric state.
124
+ * The [newState] prop is expected to contain [view]'s props as wrapped Fabric state.
109
125
  */
110
126
  @Suppress("KotlinJniMissingFunction")
111
127
  @JvmStatic
112
- external fun updateViewProps(view: ${HybridTSpec}, state: StateWrapper)
128
+ external fun updateViewProps(view: ${HybridTSpec}, newState: StateWrapper, oldState: StateWrapper?)
113
129
  }
114
130
  }
115
131
  `.trim();
@@ -143,7 +159,12 @@ public:
143
159
  public:
144
160
  static void updateViewProps(jni::alias_ref<jni::JClass> /* class */,
145
161
  jni::alias_ref<${JHybridTSpec}::JavaPart> view,
146
- jni::alias_ref<JStateWrapper::javaobject> stateWrapperInterface);
162
+ jni::alias_ref<JStateWrapper::javaobject> newState,
163
+ jni::alias_ref<JStateWrapper::javaobject> oldState);
164
+
165
+ private:
166
+ static std::shared_ptr<const ${propsClassName}> getPropsFromStateWrapper(
167
+ jni::alias_ref<JStateWrapper::javaobject> stateWrapper);
147
168
 
148
169
  public:
149
170
  static void registerNatives() {
@@ -164,9 +185,10 @@ public:
164
185
  const name = escapeCppName(p.name);
165
186
  const setter = p.getSetterName('other');
166
187
  return `
167
- if (props->${name}.isDirty) {
168
- hybridView->${setter}(props->${name}.value);
169
- props->${name}.isDirty = false;
188
+ if (oldProps == nullptr
189
+ ? newProps->${name}.isProvided()
190
+ : !newProps->${name}.hasSameValue(oldProps->${name})) {
191
+ hybridView->${setter}(newProps->${name}.get());
170
192
  }
171
193
  `.trim();
172
194
  });
@@ -183,38 +205,55 @@ namespace ${cxxNamespace} {
183
205
  using namespace facebook;
184
206
  using ConcreteStateData = react::ConcreteState<${stateClassName}>;
185
207
 
186
- void J${stateUpdaterName}::updateViewProps(jni::alias_ref<jni::JClass> /* class */,
187
- jni::alias_ref<${JHybridTSpec}::JavaPart> javaView,
188
- jni::alias_ref<JStateWrapper::javaobject> stateWrapperInterface) {
189
- std::shared_ptr<${JHybridTSpec}> hybridView = javaView->get${JHybridTSpec}();
190
-
208
+ std::shared_ptr<const ${propsClassName}> J${stateUpdaterName}::getPropsFromStateWrapper(
209
+ jni::alias_ref<JStateWrapper::javaobject> stateWrapper) {
210
+ if (stateWrapper.get() == nullptr) {
211
+ return nullptr;
212
+ }
191
213
  // Get concrete StateWrapperImpl from passed StateWrapper interface object
192
- jobject rawStateWrapper = stateWrapperInterface.get();
193
- if (!stateWrapperInterface->isInstanceOf(react::StateWrapperImpl::javaClassStatic())) [[unlikely]] {
194
- throw std::runtime_error("StateWrapper is not a StateWrapperImpl");
214
+ jobject rawStateWrapper = stateWrapper.get();
215
+ if (!stateWrapper->isInstanceOf(react::StateWrapperImpl::javaClassStatic())) [[unlikely]] {
216
+ throw std::runtime_error("StateWrapper is not a StateWrapperImpl");
217
+ }
218
+ auto stateWrapperImpl = jni::alias_ref<react::StateWrapperImpl::javaobject>{
219
+ static_cast<react::StateWrapperImpl::javaobject>(rawStateWrapper)
220
+ };
221
+ std::shared_ptr<const react::State> state = stateWrapperImpl->cthis()->getState();
222
+ if (state == nullptr) {
223
+ return nullptr;
195
224
  }
196
- auto stateWrapper = jni::alias_ref<react::StateWrapperImpl::javaobject>{
197
- static_cast<react::StateWrapperImpl::javaobject>(rawStateWrapper)};
198
- std::shared_ptr<const react::State> state = stateWrapper->cthis()->getState();
199
225
  auto concreteState = std::static_pointer_cast<const ConcreteStateData>(state);
200
226
  const ${stateClassName}& data = concreteState->getData();
201
- const std::shared_ptr<${propsClassName}>& props = data.getProps();
227
+ const std::shared_ptr<const ${propsClassName}>& props = data.getProps();
202
228
  if (props == nullptr) [[unlikely]] {
203
- // Props aren't set yet!
204
229
  throw std::runtime_error("${stateClassName}'s data doesn't contain any props!");
205
230
  }
231
+ return props;
232
+ }
233
+
234
+ void J${stateUpdaterName}::updateViewProps(jni::alias_ref<jni::JClass> /* class */,
235
+ jni::alias_ref<${JHybridTSpec}::JavaPart> javaView,
236
+ jni::alias_ref<JStateWrapper::javaobject> newState,
237
+ jni::alias_ref<JStateWrapper::javaobject> oldState) {
238
+ std::shared_ptr<${JHybridTSpec}> hybridView = javaView->get${JHybridTSpec}();
239
+ std::shared_ptr<const ${propsClassName}> newProps = getPropsFromStateWrapper(newState);
240
+ std::shared_ptr<const ${propsClassName}> oldProps = getPropsFromStateWrapper(oldState);
241
+ if (newProps == nullptr) [[unlikely]] {
242
+ throw std::runtime_error("Current StateWrapper doesn't contain any props!");
243
+ }
206
244
 
207
- // Update all props if they are dirty
245
+ // Update only props that differ from the previous State snapshot.
208
246
  ${indent(propsUpdaterCalls.join('\n'), ' ')}
209
247
 
210
248
  // Update hybridRef if it changed
211
- if (props->hybridRef.isDirty) {
249
+ if (oldProps == nullptr
250
+ ? newProps->hybridRef.isProvided()
251
+ : !newProps->hybridRef.hasSameValue(oldProps->hybridRef)) {
212
252
  // hybridRef changed - call it with new this
213
- const auto& maybeFunc = props->hybridRef.value;
253
+ const auto& maybeFunc = newProps->hybridRef.get();
214
254
  if (maybeFunc.has_value()) {
215
255
  maybeFunc.value()(hybridView);
216
256
  }
217
- props->hybridRef.isDirty = false;
218
257
  }
219
258
  }
220
259
 
@@ -10,7 +10,7 @@ export function createSwiftHybridViewManager(spec) {
10
10
  const namespace = spec.config.getCxxNamespace('c++');
11
11
  const swiftNamespace = spec.config.getIosModuleName();
12
12
  const { HybridTSpec, HybridTSpecSwift, HybridTSpecCxx } = getHybridObjectName(spec.name);
13
- const { component, descriptorClassName, propsClassName } = getViewComponentNames(spec);
13
+ const { component, descriptorClassName, propsClassName, shadowNodeClassName, } = getViewComponentNames(spec);
14
14
  const implementation = spec.config.getIosAutolinkedImplementation(spec.name);
15
15
  if (implementation?.language !== 'swift') {
16
16
  throw new Error(`Cannot create Swift HybridView ViewManager for ${spec.name} - it must be autolinked with a Swift iOS implementation in nitro.json!`);
@@ -19,12 +19,13 @@ export function createSwiftHybridViewManager(spec) {
19
19
  const name = escapeCppName(p.name);
20
20
  const setter = p.getSetterName('swift');
21
21
  const bridge = new SwiftCxxBridgedType(p.type, false);
22
- const parse = bridge.parseFromCppToSwift(`newViewProps.${name}.value`, 'c++');
22
+ const parse = bridge.parseFromCppToSwift(`newViewProps.${name}.get()`, 'c++');
23
23
  return `
24
24
  // ${p.jsSignature}
25
- if (newViewProps.${name}.isDirty) {
25
+ if (oldViewProps == nullptr
26
+ ? newViewProps.${name}.isProvided()
27
+ : !newViewProps.${name}.hasSameValue(oldViewProps->${name})) {
26
28
  swiftPart.${setter}(${indent(parse, ' ')});
27
- newViewProps.${name}.isDirty = false;
28
29
  }
29
30
  `.trim();
30
31
  });
@@ -63,6 +64,7 @@ using namespace ${namespace}::views;
63
64
 
64
65
  @implementation ${component} {
65
66
  std::shared_ptr<${HybridTSpecSwift}> _hybridView;
67
+ BOOL _didDropView;
66
68
  }
67
69
 
68
70
  + (void) load {
@@ -76,6 +78,7 @@ using namespace ${namespace}::views;
76
78
 
77
79
  - (instancetype) init {
78
80
  if (self = [super init]) {
81
+ _props = ${shadowNodeClassName}::defaultSharedProps();
79
82
  std::shared_ptr<${HybridTSpec}> hybridView = ${getHybridObjectConstructorCall(spec.name)}
80
83
  _hybridView = std::dynamic_pointer_cast<${HybridTSpecSwift}>(hybridView);
81
84
  [self updateView];
@@ -95,31 +98,50 @@ using namespace ${namespace}::views;
95
98
  [self setContentView:view];
96
99
  }
97
100
 
101
+ - (void) notifyOnDropView {
102
+ // A recycled component can later be invalidated. Notify only once per mount.
103
+ if (_didDropView) {
104
+ return;
105
+ }
106
+ ${swiftNamespace}::${HybridTSpecCxx}& swiftPart = _hybridView->getSwiftPart();
107
+ swiftPart.onDropView();
108
+ _didDropView = YES;
109
+ }
110
+
98
111
  - (void) updateProps:(const std::shared_ptr<const react::Props>&)props
99
112
  oldProps:(const std::shared_ptr<const react::Props>&)oldProps {
113
+ // A props update marks a newly mounted or still-active component.
114
+ _didDropView = NO;
115
+
100
116
  // 1. Downcast props
101
- const auto& newViewPropsConst = *std::static_pointer_cast<${propsClassName} const>(props);
102
- auto& newViewProps = const_cast<${propsClassName}&>(newViewPropsConst);
117
+ const auto& newViewProps = *std::static_pointer_cast<const ${propsClassName}>(props);
118
+ const auto* oldViewProps = static_cast<const ${propsClassName}*>(oldProps.get());
103
119
  ${swiftNamespace}::${HybridTSpecCxx}& swiftPart = _hybridView->getSwiftPart();
104
120
 
105
- // 2. Update each prop individually
106
- swiftPart.beforeUpdate();
107
-
108
- ${indent(propAssignments.join('\n'), ' ')}
109
-
110
- swiftPart.afterUpdate();
111
-
112
- // 3. Update hybridRef if it changed
113
- if (newViewProps.hybridRef.isDirty) {
114
- // hybridRef changed - call it with new this
115
- const auto& maybeFunc = newViewProps.hybridRef.value;
116
- if (maybeFunc.has_value()) {
117
- maybeFunc.value()(_hybridView);
121
+ // 2. Update only props that differ from the previous Props snapshot.
122
+ const bool hasTransactionPropChanges = oldViewProps == nullptr
123
+ ? newViewProps.hasAnyProvidedProps()
124
+ : !newViewProps.hasSameProps(*oldViewProps);
125
+ if (hasTransactionPropChanges) {
126
+ swiftPart.beforeUpdate();
127
+
128
+ ${indent(propAssignments.join('\n'), ' ')}
129
+
130
+ // Update hybridRef if it changed
131
+ if (oldViewProps == nullptr
132
+ ? newViewProps.hybridRef.isProvided()
133
+ : !newViewProps.hybridRef.hasSameValue(oldViewProps->hybridRef)) {
134
+ // hybridRef changed - call it with new this
135
+ const auto& maybeFunc = newViewProps.hybridRef.get();
136
+ if (maybeFunc.has_value()) {
137
+ maybeFunc.value()(_hybridView);
138
+ }
118
139
  }
119
- newViewProps.hybridRef.isDirty = false;
140
+
141
+ swiftPart.afterUpdate();
120
142
  }
121
143
 
122
- // 4. Continue in base class
144
+ // 3. Continue in base class
123
145
  [super updateProps:props oldProps:oldProps];
124
146
  }
125
147
 
@@ -128,6 +150,7 @@ using namespace ${namespace}::views;
128
150
  }
129
151
 
130
152
  - (void)prepareForRecycle {
153
+ [self notifyOnDropView];
131
154
  [super prepareForRecycle];
132
155
  ${swiftNamespace}::${HybridTSpecCxx}& swiftPart = _hybridView->getSwiftPart();
133
156
  swiftPart.maybePrepareForRecycle();
@@ -135,8 +158,7 @@ using namespace ${namespace}::views;
135
158
 
136
159
  #ifdef ENABLE_RCT_COMPONENT_VIEW_INVALIDATE
137
160
  - (void)invalidate {
138
- ${swiftNamespace}::${HybridTSpecCxx}& swiftPart = _hybridView->getSwiftPart();
139
- swiftPart.onDropView();
161
+ [self notifyOnDropView];
140
162
  [super invalidate];
141
163
  }
142
164
  #endif
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitrogen",
3
- "version": "0.36.5",
3
+ "version": "0.37.0-beta.0",
4
4
  "description": "Code generator for React Native Nitro Modules that turns TypeScript specs into C++, Swift, and Kotlin bindings.",
5
5
  "main": "lib/index",
6
6
  "types": "lib/index.d.ts",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "chalk": "^5.3.0",
38
- "react-native-nitro-modules": "^0.36.5",
38
+ "react-native-nitro-modules": "^0.37.0-beta.0",
39
39
  "ts-morph": "^28.0.0",
40
40
  "yargs": "^18.0.0",
41
41
  "zod": "^4.4.3"
@@ -69,6 +69,16 @@ function getFunctionCallSignature(func: TSMorphType): Signature {
69
69
  return callSignature
70
70
  }
71
71
 
72
+ /**
73
+ * Whether the given type is optional (`T | undefined`) or not.
74
+ *
75
+ * Note that `null` is a separate type in Nitro (`NullType`) - only `undefined`
76
+ * (or a `?`) makes a type optional.
77
+ */
78
+ function isOptionalType(type: TSMorphType): boolean {
79
+ return type.getUnionTypes().some((t) => t.isUndefined())
80
+ }
81
+
72
82
  function removeDuplicates(types: Type[]): Type[] {
73
83
  return types.filter((t1, index, array) => {
74
84
  const firstIndexOfType = array.findIndex(
@@ -269,7 +279,11 @@ export function createType(
269
279
  return new VoidType()
270
280
  } else if (type.isArray()) {
271
281
  const arrayElementType = type.getArrayElementTypeOrThrow()
272
- const elementType = createType(language, arrayElementType, false)
282
+ const elementType = createType(
283
+ language,
284
+ arrayElementType,
285
+ isOptionalType(arrayElementType)
286
+ )
273
287
  return new ArrayType(elementType)
274
288
  } else if (type.isTuple()) {
275
289
  const itemTypes = type
@@ -280,10 +294,11 @@ export function createType(
280
294
  // It's a function!
281
295
  const callSignature = getFunctionCallSignature(type)
282
296
  const funcReturnType = callSignature.getReturnType()
283
- const isReturnOptional = funcReturnType
284
- .getUnionTypes()
285
- .some((t) => t.isUndefined())
286
- const returnType = createType(language, funcReturnType, isReturnOptional)
297
+ const returnType = createType(
298
+ language,
299
+ funcReturnType,
300
+ isOptionalType(funcReturnType)
301
+ )
287
302
  const parameters = callSignature.getParameters().map((p) => {
288
303
  const declaration = p.getValueDeclarationOrThrow()
289
304
  const parameterType = p.getTypeAtLocation(declaration)
@@ -295,20 +310,21 @@ export function createType(
295
310
  } else if (isPromise(type)) {
296
311
  // It's a Promise!
297
312
  const [promiseResolvingType] = getArguments(type, 'Promise', 1)
298
- const isResolvingOptional = promiseResolvingType
299
- .getUnionTypes()
300
- .some((t) => t.isUndefined())
301
313
  const resolvingType = createType(
302
314
  language,
303
315
  promiseResolvingType,
304
- isResolvingOptional
316
+ isOptionalType(promiseResolvingType)
305
317
  )
306
318
  return new PromiseType(resolvingType)
307
319
  } else if (isRecord(type)) {
308
320
  // Record<K, V> -> unordered_map<K, V>
309
321
  const [keyTypeT, valueTypeT] = getArguments(type, 'Record', 2)
310
322
  const keyType = createType(language, keyTypeT, false)
311
- const valueType = createType(language, valueTypeT, false)
323
+ const valueType = createType(
324
+ language,
325
+ valueTypeT,
326
+ isOptionalType(valueTypeT)
327
+ )
312
328
  return new RecordType(keyType, valueType)
313
329
  } else if (isArrayBuffer(type)) {
314
330
  // ArrayBuffer
@@ -469,6 +469,13 @@ export class KotlinCxxBridgedType implements BridgedType<'kotlin', 'c++'> {
469
469
 
470
470
  dereferenceToJObject(parameterName: string): string {
471
471
  switch (this.type.kind) {
472
+ // an optional bridges to a (possibly `nullptr`) ref of its wrapping type,
473
+ // so it needs to be dereferenced the same way as the type it wraps.
474
+ case 'optional': {
475
+ const optional = getTypeAs(this.type, OptionalType)
476
+ const bridge = new KotlinCxxBridgedType(optional.wrappingType)
477
+ return bridge.dereferenceToJObject(parameterName)
478
+ }
472
479
  // any jni::HybridClass needs to be dereferenced to jobject with .get()
473
480
  case 'array-buffer':
474
481
  case 'function':
@@ -4,7 +4,6 @@ import { createIndentation, indent } from '../utils.js'
4
4
  import {
5
5
  createFileMetadataString,
6
6
  escapeCppName,
7
- isFunction,
8
7
  isNotDuplicate,
9
8
  } from '../syntax/helpers.js'
10
9
  import { getHybridObjectName } from '../syntax/getHybridObjectName.js'
@@ -73,9 +72,20 @@ export function createViewComponentShadowNodeFiles(
73
72
 
74
73
  const props = [...spec.properties, getHybridRefProperty(spec)]
75
74
  const properties = props.map(
76
- (p) => `CachedProp<${p.type.getCode('c++')}> ${escapeCppName(p.name)};`
75
+ (p) =>
76
+ `nitro::CachedProp<${p.type.getCode('c++')}> ${escapeCppName(p.name)};`
77
77
  )
78
- const cases = props.map((p) => `case hashString("${p.name}"): return true;`)
78
+ const filterCases = props.map(
79
+ (prop) => `case hashString("${prop.name}"): return true;`
80
+ )
81
+ const comparisons = props.map((prop) => {
82
+ const name = escapeCppName(prop.name)
83
+ return `${name}.hasSameValue(other.${name})`
84
+ })
85
+ const providedChecks = props.map((prop) => {
86
+ const name = escapeCppName(prop.name)
87
+ return `${name}.isProvided()`
88
+ })
79
89
  const includes = props
80
90
  .flatMap((p) =>
81
91
  p.getRequiredImports('c++').map((i) => includeHeader(i, true))
@@ -89,14 +99,15 @@ ${createFileMetadataString(`${component}.hpp`)}
89
99
 
90
100
  #pragma once
91
101
 
92
- #include <optional>
93
- #include <NitroModules/NitroDefines.hpp>
94
- #include <NitroModules/NitroHash.hpp>
95
102
  #include <NitroModules/CachedProp.hpp>
96
- #include <react/renderer/core/ConcreteComponentDescriptor.h>
97
- #include <react/renderer/core/PropsParserContext.h>
103
+ #include <NitroModules/ViewComponentDescriptor.hpp>
104
+ #include <NitroModules/ViewPropsHolderState.hpp>
98
105
  #include <react/renderer/components/view/ConcreteViewShadowNode.h>
99
106
  #include <react/renderer/components/view/ViewProps.h>
107
+ #include <react/renderer/core/PropsParserContext.h>
108
+ #include <react/renderer/core/RawProps.h>
109
+
110
+ #include <string>
100
111
 
101
112
  ${includes.join('\n')}
102
113
 
@@ -122,6 +133,16 @@ namespace ${namespace} {
122
133
  public:
123
134
  ${indent(properties.join('\n'), ' ')}
124
135
 
136
+ [[nodiscard]]
137
+ bool hasSameProps(const ${propsClassName}& other) const noexcept {
138
+ return ${comparisons.join(' &&\n ')};
139
+ }
140
+
141
+ [[nodiscard]]
142
+ bool hasAnyProvidedProps() const noexcept {
143
+ return ${providedChecks.join(' ||\n ')};
144
+ }
145
+
125
146
  private:
126
147
  static bool filterObjectKeys(const std::string& propName);
127
148
  };
@@ -129,32 +150,7 @@ namespace ${namespace} {
129
150
  /**
130
151
  * State for the "${spec.name}" View.
131
152
  */
132
- class ${stateClassName} final {
133
- public:
134
- ${stateClassName}() = default;
135
- explicit ${stateClassName}(const std::shared_ptr<${propsClassName}>& props):
136
- _props(props) {}
137
-
138
- public:
139
- [[nodiscard]]
140
- const std::shared_ptr<${propsClassName}>& getProps() const {
141
- return _props;
142
- }
143
-
144
- public:
145
- #ifdef ANDROID
146
- ${stateClassName}(const ${stateClassName}& /* previousState */, folly::dynamic /* data */) {}
147
- folly::dynamic getDynamic() const {
148
- throw std::runtime_error("${stateClassName} does not support folly!");
149
- }
150
- react::MapBuffer getMapBuffer() const {
151
- throw std::runtime_error("${stateClassName} does not support MapBuffer!");
152
- };
153
- #endif
154
-
155
- private:
156
- std::shared_ptr<${propsClassName}> _props;
157
- };
153
+ using ${stateClassName} = nitro::ViewPropsHolderState<${propsClassName}>;
158
154
 
159
155
  /**
160
156
  * The Shadow Node for the "${spec.name}" View.
@@ -167,80 +163,34 @@ namespace ${namespace} {
167
163
  /**
168
164
  * The Component Descriptor for the "${spec.name}" View.
169
165
  */
170
- class ${descriptorClassName} final: public react::ConcreteComponentDescriptor<${shadowNodeClassName}> {
171
- public:
172
- explicit ${descriptorClassName}(const react::ComponentDescriptorParameters& parameters);
173
-
174
- public:
175
- /**
176
- * A faster path for cloning props - reuses the caching logic from \`${propsClassName}\`.
177
- */
178
- std::shared_ptr<const react::Props> cloneProps(const react::PropsParserContext& context,
179
- const std::shared_ptr<const react::Props>& props,
180
- react::RawProps rawProps) const override;
181
- #ifdef ANDROID
182
- void adopt(react::ShadowNode& shadowNode) const override;
183
- #endif
184
- };
166
+ using ${descriptorClassName} = nitro::ViewComponentDescriptor<${shadowNodeClassName}>;
185
167
 
186
168
  /* The actual view for "${spec.name}" needs to be implemented in platform-specific code. */
187
169
 
188
170
  } // namespace ${namespace}
189
171
  `.trim()
190
172
 
191
- // .cpp code
192
173
  const propInitializers = [
193
174
  'react::ViewProps(context, sourceProps, rawProps, filterObjectKeys)',
175
+ ...props.map((prop) => {
176
+ const name = escapeCppName(prop.name)
177
+ const type = prop.type.getCode('c++')
178
+ return `${name}(nitro::CachedProp<${type}>::fromRawValue("${spec.name}", "${prop.name}", rawProps, sourceProps.${name}))`
179
+ }),
194
180
  ]
195
- const propCopyInitializers = ['react::ViewProps()']
196
- for (const prop of props) {
197
- const name = escapeCppName(prop.name)
198
- const type = prop.type.getCode('c++')
199
-
200
- let valueConversion = `value`
201
- if (isFunction(prop.type)) {
202
- // Due to a React limitation, functions cannot be passed to native directly,
203
- // because RN converts them to booleans (`true`). Nitro knows this and just
204
- // wraps functions as objects - the original function is stored in `f`.
205
- valueConversion = `value.asObject(*runtime).getProperty(*runtime, PropNameIDCache::get(*runtime, "f"))`
206
- }
207
-
208
- propInitializers.push(
209
- `
210
- ${name}([&]() -> CachedProp<${type}> {
211
- try {
212
- const react::RawValue* rawValue = rawProps.at("${prop.name}", nullptr, nullptr);
213
- if (rawValue == nullptr) return sourceProps.${name};
214
- const auto& [runtime, value] = (std::pair<jsi::Runtime*, jsi::Value>)*rawValue;
215
- return CachedProp<${type}>::fromRawValue(*runtime, ${valueConversion}, sourceProps.${name});
216
- } catch (const std::exception& exc) {
217
- throw std::runtime_error(std::string("${spec.name}.${prop.name}: ") + exc.what());
218
- }
219
- }())`.trim()
220
- )
221
- propCopyInitializers.push(`${name}(other.${name})`)
222
- }
223
-
224
181
  const ctorIndent = createIndentation(propsClassName.length * 2)
225
- const descriptorIndent = createIndentation(descriptorClassName.length)
226
182
  const componentCode = `
227
183
  ${createFileMetadataString(`${component}.cpp`)}
228
184
 
229
185
  #include "${component}.hpp"
230
186
 
231
- #include <string>
232
- #include <exception>
233
- #include <utility>
234
- #include <NitroModules/NitroDefines.hpp>
235
- #include <NitroModules/JSIConverter.hpp>
236
- #include <NitroModules/PropNameIDCache.hpp>
237
- #include <react/renderer/core/RawValue.h>
238
- #include <react/renderer/core/ShadowNode.h>
239
- #include <react/renderer/core/ComponentDescriptor.h>
240
- #include <react/renderer/components/view/ViewProps.h>
187
+ #include <NitroModules/NitroHash.hpp>
188
+ #include <NitroModules/CachedProp.hpp>
241
189
 
242
190
  namespace ${namespace} {
243
191
 
192
+ using namespace facebook;
193
+
244
194
  extern const char ${nameVariable}[] = "${T}";
245
195
 
246
196
  ${propsClassName}::${propsClassName}(const react::PropsParserContext& context,
@@ -250,36 +200,11 @@ namespace ${namespace} {
250
200
 
251
201
  bool ${propsClassName}::filterObjectKeys(const std::string& propName) {
252
202
  switch (hashString(propName)) {
253
- ${indent(cases.join('\n'), ' ')}
203
+ ${indent(filterCases.join('\n'), ' ')}
254
204
  default: return false;
255
205
  }
256
206
  }
257
207
 
258
- ${descriptorClassName}::${descriptorClassName}(const react::ComponentDescriptorParameters& parameters)
259
- : ConcreteComponentDescriptor(parameters,
260
- react::RawPropsParser()) {}
261
-
262
- std::shared_ptr<const react::Props> ${descriptorClassName}::cloneProps(const react::PropsParserContext& context,
263
- ${descriptorIndent} const std::shared_ptr<const react::Props>& props,
264
- ${descriptorIndent} react::RawProps rawProps) const {
265
- // 1. Prepare raw props parser
266
- rawProps.parse(rawPropsParser_);
267
- // 2. Copy props with Nitro's cached copy constructor
268
- return ${shadowNodeClassName}::Props(context, /* & */ rawProps, props);
269
- }
270
-
271
- #ifdef ANDROID
272
- void ${descriptorClassName}::adopt(react::ShadowNode& shadowNode) const {
273
- // This is called immediately after \`ShadowNode\` is created, cloned or in progress.
274
- // On Android, we need to wrap props in our state, which gets routed through Java and later unwrapped in JNI/C++.
275
- auto& concreteShadowNode = static_cast<${shadowNodeClassName}&>(shadowNode);
276
- const std::shared_ptr<const ${propsClassName}>& constProps = concreteShadowNode.getConcreteSharedProps();
277
- const std::shared_ptr<${propsClassName}>& props = std::const_pointer_cast<${propsClassName}>(constProps);
278
- ${stateClassName} state{props};
279
- concreteShadowNode.setStateData(std::move(state));
280
- }
281
- #endif
282
-
283
208
  } // namespace ${namespace}
284
209
  `.trim()
285
210
 
@@ -56,6 +56,14 @@ import ${javaNamespace}.*
56
56
  * Represents the React Native \`ViewManager\` for the "${spec.name}" Nitro HybridView.
57
57
  */
58
58
  public class ${manager}: SimpleViewManager<View>() {
59
+ /**
60
+ * Represents the View and its last state snapshot (mutable)
61
+ */
62
+ private class HybridViewHolder(
63
+ val hybridView: ${viewImplementation},
64
+ var lastState: StateWrapper? = null,
65
+ )
66
+
59
67
  init {
60
68
  if (RecyclableView::class.java.isAssignableFrom(${viewImplementation}::class.java)) {
61
69
  // Enable view recycling
@@ -70,33 +78,41 @@ public class ${manager}: SimpleViewManager<View>() {
70
78
  override fun createViewInstance(reactContext: ThemedReactContext): View {
71
79
  val hybridView = ${viewImplementation}(reactContext)
72
80
  val view = hybridView.view
73
- view.setTag(associated_hybrid_view_tag, hybridView)
81
+ view.setTag(associated_hybrid_view_tag, HybridViewHolder(hybridView))
74
82
  return view
75
83
  }
76
84
 
77
85
  override fun updateState(view: View, props: ReactStylesDiffMap, stateWrapper: StateWrapper): Any? {
78
- val hybridView = getHybridView(view)
86
+ val holder = getHybridViewHolder(view)
79
87
  ?: throw Error("Couldn't find view $view in local views table!")
88
+ val hybridView = holder.hybridView
89
+ val oldState = holder.lastState
90
+ val newState = stateWrapper
80
91
 
81
92
  // 1. Update each prop individually
82
93
  hybridView.beforeUpdate()
83
- ${stateUpdaterName}.updateViewProps(hybridView, stateWrapper)
94
+ ${stateUpdaterName}.updateViewProps(hybridView, newState, oldState)
84
95
  hybridView.afterUpdate()
96
+ holder.lastState = newState
85
97
 
86
98
  // 2. Continue in base View props
87
- return super.updateState(view, props, stateWrapper)
99
+ return super.updateState(view, props, newState)
88
100
  }
89
101
 
90
102
  override fun onDropViewInstance(view: View) {
91
- val hybridView = getHybridView(view)
92
- hybridView?.onDropView()
103
+ val holder = getHybridViewHolder(view)
104
+ holder?.lastState = null
105
+ holder?.hybridView?.onDropView()
93
106
  return super.onDropViewInstance(view)
94
107
  }
95
108
 
96
109
  protected override fun prepareToRecycleView(reactContext: ThemedReactContext, view: View): View? {
97
- super.prepareToRecycleView(reactContext, view)
98
- val hybridView = getHybridView(view)
110
+ val preparedView = super.prepareToRecycleView(reactContext, view)
99
111
  ?: return null
112
+ val holder = getHybridViewHolder(preparedView)
113
+ ?: return null
114
+ val hybridView = holder.hybridView
115
+ holder.lastState = null
100
116
 
101
117
  @Suppress("USELESS_IS_CHECK")
102
118
  if (hybridView is RecyclableView) {
@@ -110,8 +126,8 @@ public class ${manager}: SimpleViewManager<View>() {
110
126
  }
111
127
  }
112
128
 
113
- private fun getHybridView(view: View): ${viewImplementation}? {
114
- return view.getTag(associated_hybrid_view_tag) as? ${viewImplementation}
129
+ private fun getHybridViewHolder(view: View): HybridViewHolder? {
130
+ return view.getTag(associated_hybrid_view_tag) as? HybridViewHolder
115
131
  }
116
132
  }
117
133
  `.trim()
@@ -128,11 +144,11 @@ internal class ${stateUpdaterName} {
128
144
  companion object {
129
145
  /**
130
146
  * Updates the props for [view] through C++.
131
- * The [state] prop is expected to contain [view]'s props as wrapped Fabric state.
147
+ * The [newState] prop is expected to contain [view]'s props as wrapped Fabric state.
132
148
  */
133
149
  @Suppress("KotlinJniMissingFunction")
134
150
  @JvmStatic
135
- external fun updateViewProps(view: ${HybridTSpec}, state: StateWrapper)
151
+ external fun updateViewProps(view: ${HybridTSpec}, newState: StateWrapper, oldState: StateWrapper?)
136
152
  }
137
153
  }
138
154
  `.trim()
@@ -171,7 +187,12 @@ public:
171
187
  public:
172
188
  static void updateViewProps(jni::alias_ref<jni::JClass> /* class */,
173
189
  jni::alias_ref<${JHybridTSpec}::JavaPart> view,
174
- jni::alias_ref<JStateWrapper::javaobject> stateWrapperInterface);
190
+ jni::alias_ref<JStateWrapper::javaobject> newState,
191
+ jni::alias_ref<JStateWrapper::javaobject> oldState);
192
+
193
+ private:
194
+ static std::shared_ptr<const ${propsClassName}> getPropsFromStateWrapper(
195
+ jni::alias_ref<JStateWrapper::javaobject> stateWrapper);
175
196
 
176
197
  public:
177
198
  static void registerNatives() {
@@ -193,9 +214,10 @@ public:
193
214
  const name = escapeCppName(p.name)
194
215
  const setter = p.getSetterName('other')
195
216
  return `
196
- if (props->${name}.isDirty) {
197
- hybridView->${setter}(props->${name}.value);
198
- props->${name}.isDirty = false;
217
+ if (oldProps == nullptr
218
+ ? newProps->${name}.isProvided()
219
+ : !newProps->${name}.hasSameValue(oldProps->${name})) {
220
+ hybridView->${setter}(newProps->${name}.get());
199
221
  }
200
222
  `.trim()
201
223
  })
@@ -212,38 +234,55 @@ namespace ${cxxNamespace} {
212
234
  using namespace facebook;
213
235
  using ConcreteStateData = react::ConcreteState<${stateClassName}>;
214
236
 
215
- void J${stateUpdaterName}::updateViewProps(jni::alias_ref<jni::JClass> /* class */,
216
- jni::alias_ref<${JHybridTSpec}::JavaPart> javaView,
217
- jni::alias_ref<JStateWrapper::javaobject> stateWrapperInterface) {
218
- std::shared_ptr<${JHybridTSpec}> hybridView = javaView->get${JHybridTSpec}();
219
-
237
+ std::shared_ptr<const ${propsClassName}> J${stateUpdaterName}::getPropsFromStateWrapper(
238
+ jni::alias_ref<JStateWrapper::javaobject> stateWrapper) {
239
+ if (stateWrapper.get() == nullptr) {
240
+ return nullptr;
241
+ }
220
242
  // Get concrete StateWrapperImpl from passed StateWrapper interface object
221
- jobject rawStateWrapper = stateWrapperInterface.get();
222
- if (!stateWrapperInterface->isInstanceOf(react::StateWrapperImpl::javaClassStatic())) [[unlikely]] {
223
- throw std::runtime_error("StateWrapper is not a StateWrapperImpl");
243
+ jobject rawStateWrapper = stateWrapper.get();
244
+ if (!stateWrapper->isInstanceOf(react::StateWrapperImpl::javaClassStatic())) [[unlikely]] {
245
+ throw std::runtime_error("StateWrapper is not a StateWrapperImpl");
246
+ }
247
+ auto stateWrapperImpl = jni::alias_ref<react::StateWrapperImpl::javaobject>{
248
+ static_cast<react::StateWrapperImpl::javaobject>(rawStateWrapper)
249
+ };
250
+ std::shared_ptr<const react::State> state = stateWrapperImpl->cthis()->getState();
251
+ if (state == nullptr) {
252
+ return nullptr;
224
253
  }
225
- auto stateWrapper = jni::alias_ref<react::StateWrapperImpl::javaobject>{
226
- static_cast<react::StateWrapperImpl::javaobject>(rawStateWrapper)};
227
- std::shared_ptr<const react::State> state = stateWrapper->cthis()->getState();
228
254
  auto concreteState = std::static_pointer_cast<const ConcreteStateData>(state);
229
255
  const ${stateClassName}& data = concreteState->getData();
230
- const std::shared_ptr<${propsClassName}>& props = data.getProps();
256
+ const std::shared_ptr<const ${propsClassName}>& props = data.getProps();
231
257
  if (props == nullptr) [[unlikely]] {
232
- // Props aren't set yet!
233
258
  throw std::runtime_error("${stateClassName}'s data doesn't contain any props!");
234
259
  }
260
+ return props;
261
+ }
262
+
263
+ void J${stateUpdaterName}::updateViewProps(jni::alias_ref<jni::JClass> /* class */,
264
+ jni::alias_ref<${JHybridTSpec}::JavaPart> javaView,
265
+ jni::alias_ref<JStateWrapper::javaobject> newState,
266
+ jni::alias_ref<JStateWrapper::javaobject> oldState) {
267
+ std::shared_ptr<${JHybridTSpec}> hybridView = javaView->get${JHybridTSpec}();
268
+ std::shared_ptr<const ${propsClassName}> newProps = getPropsFromStateWrapper(newState);
269
+ std::shared_ptr<const ${propsClassName}> oldProps = getPropsFromStateWrapper(oldState);
270
+ if (newProps == nullptr) [[unlikely]] {
271
+ throw std::runtime_error("Current StateWrapper doesn't contain any props!");
272
+ }
235
273
 
236
- // Update all props if they are dirty
274
+ // Update only props that differ from the previous State snapshot.
237
275
  ${indent(propsUpdaterCalls.join('\n'), ' ')}
238
276
 
239
277
  // Update hybridRef if it changed
240
- if (props->hybridRef.isDirty) {
278
+ if (oldProps == nullptr
279
+ ? newProps->hybridRef.isProvided()
280
+ : !newProps->hybridRef.hasSameValue(oldProps->hybridRef)) {
241
281
  // hybridRef changed - call it with new this
242
- const auto& maybeFunc = props->hybridRef.value;
282
+ const auto& maybeFunc = newProps->hybridRef.get();
243
283
  if (maybeFunc.has_value()) {
244
284
  maybeFunc.value()(hybridView);
245
285
  }
246
- props->hybridRef.isDirty = false;
247
286
  }
248
287
  }
249
288
 
@@ -26,8 +26,12 @@ export function createSwiftHybridViewManager(
26
26
  const { HybridTSpec, HybridTSpecSwift, HybridTSpecCxx } = getHybridObjectName(
27
27
  spec.name
28
28
  )
29
- const { component, descriptorClassName, propsClassName } =
30
- getViewComponentNames(spec)
29
+ const {
30
+ component,
31
+ descriptorClassName,
32
+ propsClassName,
33
+ shadowNodeClassName,
34
+ } = getViewComponentNames(spec)
31
35
  const implementation = spec.config.getIosAutolinkedImplementation(spec.name)
32
36
  if (implementation?.language !== 'swift') {
33
37
  throw new Error(
@@ -40,18 +44,18 @@ export function createSwiftHybridViewManager(
40
44
  const setter = p.getSetterName('swift')
41
45
  const bridge = new SwiftCxxBridgedType(p.type, false)
42
46
  const parse = bridge.parseFromCppToSwift(
43
- `newViewProps.${name}.value`,
47
+ `newViewProps.${name}.get()`,
44
48
  'c++'
45
49
  )
46
50
  return `
47
51
  // ${p.jsSignature}
48
- if (newViewProps.${name}.isDirty) {
52
+ if (oldViewProps == nullptr
53
+ ? newViewProps.${name}.isProvided()
54
+ : !newViewProps.${name}.hasSameValue(oldViewProps->${name})) {
49
55
  swiftPart.${setter}(${indent(parse, ' ')});
50
- newViewProps.${name}.isDirty = false;
51
56
  }
52
57
  `.trim()
53
58
  })
54
-
55
59
  const mmFile = `
56
60
  ${createFileMetadataString(`${component}.mm`)}
57
61
 
@@ -87,6 +91,7 @@ using namespace ${namespace}::views;
87
91
 
88
92
  @implementation ${component} {
89
93
  std::shared_ptr<${HybridTSpecSwift}> _hybridView;
94
+ BOOL _didDropView;
90
95
  }
91
96
 
92
97
  + (void) load {
@@ -100,6 +105,7 @@ using namespace ${namespace}::views;
100
105
 
101
106
  - (instancetype) init {
102
107
  if (self = [super init]) {
108
+ _props = ${shadowNodeClassName}::defaultSharedProps();
103
109
  std::shared_ptr<${HybridTSpec}> hybridView = ${getHybridObjectConstructorCall(spec.name)}
104
110
  _hybridView = std::dynamic_pointer_cast<${HybridTSpecSwift}>(hybridView);
105
111
  [self updateView];
@@ -119,31 +125,50 @@ using namespace ${namespace}::views;
119
125
  [self setContentView:view];
120
126
  }
121
127
 
128
+ - (void) notifyOnDropView {
129
+ // A recycled component can later be invalidated. Notify only once per mount.
130
+ if (_didDropView) {
131
+ return;
132
+ }
133
+ ${swiftNamespace}::${HybridTSpecCxx}& swiftPart = _hybridView->getSwiftPart();
134
+ swiftPart.onDropView();
135
+ _didDropView = YES;
136
+ }
137
+
122
138
  - (void) updateProps:(const std::shared_ptr<const react::Props>&)props
123
139
  oldProps:(const std::shared_ptr<const react::Props>&)oldProps {
140
+ // A props update marks a newly mounted or still-active component.
141
+ _didDropView = NO;
142
+
124
143
  // 1. Downcast props
125
- const auto& newViewPropsConst = *std::static_pointer_cast<${propsClassName} const>(props);
126
- auto& newViewProps = const_cast<${propsClassName}&>(newViewPropsConst);
144
+ const auto& newViewProps = *std::static_pointer_cast<const ${propsClassName}>(props);
145
+ const auto* oldViewProps = static_cast<const ${propsClassName}*>(oldProps.get());
127
146
  ${swiftNamespace}::${HybridTSpecCxx}& swiftPart = _hybridView->getSwiftPart();
128
147
 
129
- // 2. Update each prop individually
130
- swiftPart.beforeUpdate();
131
-
132
- ${indent(propAssignments.join('\n'), ' ')}
133
-
134
- swiftPart.afterUpdate();
135
-
136
- // 3. Update hybridRef if it changed
137
- if (newViewProps.hybridRef.isDirty) {
138
- // hybridRef changed - call it with new this
139
- const auto& maybeFunc = newViewProps.hybridRef.value;
140
- if (maybeFunc.has_value()) {
141
- maybeFunc.value()(_hybridView);
148
+ // 2. Update only props that differ from the previous Props snapshot.
149
+ const bool hasTransactionPropChanges = oldViewProps == nullptr
150
+ ? newViewProps.hasAnyProvidedProps()
151
+ : !newViewProps.hasSameProps(*oldViewProps);
152
+ if (hasTransactionPropChanges) {
153
+ swiftPart.beforeUpdate();
154
+
155
+ ${indent(propAssignments.join('\n'), ' ')}
156
+
157
+ // Update hybridRef if it changed
158
+ if (oldViewProps == nullptr
159
+ ? newViewProps.hybridRef.isProvided()
160
+ : !newViewProps.hybridRef.hasSameValue(oldViewProps->hybridRef)) {
161
+ // hybridRef changed - call it with new this
162
+ const auto& maybeFunc = newViewProps.hybridRef.get();
163
+ if (maybeFunc.has_value()) {
164
+ maybeFunc.value()(_hybridView);
165
+ }
142
166
  }
143
- newViewProps.hybridRef.isDirty = false;
167
+
168
+ swiftPart.afterUpdate();
144
169
  }
145
170
 
146
- // 4. Continue in base class
171
+ // 3. Continue in base class
147
172
  [super updateProps:props oldProps:oldProps];
148
173
  }
149
174
 
@@ -152,6 +177,7 @@ using namespace ${namespace}::views;
152
177
  }
153
178
 
154
179
  - (void)prepareForRecycle {
180
+ [self notifyOnDropView];
155
181
  [super prepareForRecycle];
156
182
  ${swiftNamespace}::${HybridTSpecCxx}& swiftPart = _hybridView->getSwiftPart();
157
183
  swiftPart.maybePrepareForRecycle();
@@ -159,8 +185,7 @@ using namespace ${namespace}::views;
159
185
 
160
186
  #ifdef ENABLE_RCT_COMPONENT_VIEW_INVALIDATE
161
187
  - (void)invalidate {
162
- ${swiftNamespace}::${HybridTSpecCxx}& swiftPart = _hybridView->getSwiftPart();
163
- swiftPart.onDropView();
188
+ [self notifyOnDropView];
164
189
  [super invalidate];
165
190
  }
166
191
  #endif