react-native-list 2.0.0-alpha.1 → 2.0.0-alpha.2
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.
- package/README.md +26 -28
- package/lib/ReactFabricMirror.d.ts +4 -0
- package/lib/ReactFabricMirror.js +515 -0
- package/lib/UiListModule.d.ts +2 -0
- package/lib/UiListModule.js +2 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +21 -0
- package/lib/privateGlobals.d.ts +14 -0
- package/lib/privateGlobals.js +2 -0
- package/lib/renderer/RenderHelper.d.ts +2 -0
- package/lib/renderer/RenderHelper.js +11 -0
- package/lib/renderer/UiManagerHelper.d.ts +2 -0
- package/lib/renderer/UiManagerHelper.js +2 -0
- package/lib/renderer/fabric/RenderHelper.d.ts +2 -0
- package/lib/renderer/fabric/RenderHelper.js +11 -0
- package/lib/renderer/fabric/UiManagerHelper.d.ts +2 -0
- package/lib/renderer/fabric/UiManagerHelper.js +2 -0
- package/lib/renderer/react/ReactFabricMirror.d.ts +4 -0
- package/lib/renderer/react/ReactFabricMirror.js +515 -0
- package/lib/renderer/react/ReactFabricRenderer.d.ts +3 -0
- package/lib/renderer/react/ReactFabricRenderer.js +9 -0
- package/lib/specs/IOSWorkletsModuleProxyHolder.nitro.d.ts +6 -0
- package/lib/specs/IOSWorkletsModuleProxyHolder.nitro.js +1 -0
- package/lib/specs/UIListModule.nitro.d.ts +9 -0
- package/lib/specs/UIListModule.nitro.js +1 -0
- package/lib/specs/UIManagerHelper.nitro.d.ts +13 -0
- package/lib/specs/UIManagerHelper.nitro.js +1 -0
- package/lib/specs/UiListView.nitro.d.ts +20 -0
- package/lib/specs/UiListView.nitro.js +1 -0
- package/lib/specs/ViewHolder.nitro.d.ts +6 -0
- package/lib/specs/ViewHolder.nitro.js +1 -0
- package/lib/views/List.d.ts +35 -0
- package/lib/views/List.js +225 -0
- package/lib/views/UiListHostComponent.d.ts +2 -0
- package/lib/views/UiListHostComponent.js +3 -0
- package/metro-config.js +77 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# React Native List
|
|
2
2
|
|
|
3
|
-
High-performance list
|
|
3
|
+
High-performance list for React Native.
|
|
4
4
|
|
|
5
5
|
- 📱 True native [`UICollectionView`](https://developer.apple.com/documentation/uikit/uicollectionview) on iOS
|
|
6
6
|
- 🤖 True native [`RecyclerView`](https://developer.android.com/develop/ui/views/layout/recyclerview) on android
|
|
@@ -8,26 +8,15 @@ High-performance list primitives for React Native.
|
|
|
8
8
|
- 🐎 Platform native animations out of the box for list item transitions
|
|
9
9
|
- 📉 Low memory usage due to true native view recycling
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
https://github.com/user-attachments/assets/4a2684bf-7337-45e0-bb17-ee7b8a382943
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
15
|
> [!WARNING]
|
|
16
16
|
> Using this library requires you to use [react-native-worklets **bundle mode**](https://docs.swmansion.com/react-native-worklets/docs/bundleMode/setup).
|
|
17
17
|
>
|
|
18
|
-
> - Please follow the setup instructions of it first, and make sure your app works before using react-native-list!
|
|
19
|
-
> - You need at least version 0.
|
|
20
|
-
|
|
21
|
-
> [!CAUTION]
|
|
22
|
-
> Right now you will have to patch react-native-worklets. See the patch file here and ask your friendly AI to apply it:
|
|
23
|
-
>
|
|
24
|
-
> - [react-native-worklets@0.9.1.patch](https://github.com/hannojg/react-native-list/blob/main/patches/react-native-worklets%400.9.1.patch)
|
|
25
|
-
>
|
|
26
|
-
> Once those PRs are landed upstream it will no longer be necessary:
|
|
27
|
-
>
|
|
28
|
-
> - [fix(bundlemode): getBundleModeMetroConfig consider user config's resolveRequest](https://github.com/software-mansion/react-native-reanimated/pull/9327)
|
|
29
|
-
> - [worklets plugin: allow react-native imports in bundle mode](https://github.com/software-mansion/react-native-reanimated/pull/9213)
|
|
30
|
-
> - [worklets plugin: Add opt-in bundle mode JSX component capture](https://github.com/software-mansion/react-native-reanimated/pull/9212)
|
|
18
|
+
> - Please follow [the setup instructions of it first](https://docs.swmansion.com/react-native-worklets/docs/bundleMode/setup), and make sure your app works before using react-native-list!
|
|
19
|
+
> - You need at least version 0.10.0!
|
|
31
20
|
|
|
32
21
|
Once that's out of the way, you can start with the regular setup of the library:
|
|
33
22
|
|
|
@@ -41,8 +30,15 @@ Simply wrap your config with `getReactNativeListMetroConfig` in `metro.config.js
|
|
|
41
30
|
|
|
42
31
|
```js
|
|
43
32
|
const { getDefaultConfig } = require("expo/metro-config");
|
|
44
|
-
const
|
|
45
|
-
|
|
33
|
+
const {
|
|
34
|
+
getBundleModeMetroConfig,
|
|
35
|
+
} = require("react-native-worklets/bundleMode");
|
|
36
|
+
|
|
37
|
+
let config = getDefaultConfig(__dirname);
|
|
38
|
+
config = getBundleModeMetroConfig(config);
|
|
39
|
+
// ⚠️ Make sure to apply this _after_ the bundle mode metro config:
|
|
40
|
+
config = getReactNativeListMetroConfig(config);
|
|
41
|
+
module.exports = config;
|
|
46
42
|
```
|
|
47
43
|
|
|
48
44
|
> [!NOTE]
|
|
@@ -87,9 +83,10 @@ type Items = TextItem | ImageItem
|
|
|
87
83
|
const renderers: ListRenderers<Items> = {
|
|
88
84
|
text: {
|
|
89
85
|
renderItemWorklet: ({ item }) => {
|
|
90
|
-
"worklet";
|
|
86
|
+
"worklet"; // 👀 Note: our render function is a worklet!
|
|
91
87
|
|
|
92
88
|
return (
|
|
89
|
+
// Though being in a worklet, you can use any component you like in here
|
|
93
90
|
<View
|
|
94
91
|
style={{
|
|
95
92
|
justifyContent: "center",
|
|
@@ -131,20 +128,20 @@ export function ExampleList() {
|
|
|
131
128
|
|
|
132
129
|
## API
|
|
133
130
|
|
|
134
|
-
XXXX
|
|
131
|
+
XXXX TODO
|
|
135
132
|
|
|
136
133
|
## Benchmark
|
|
137
134
|
|
|
138
135
|
> Scrolling as fast as possible. Release build. iPhone 13 Pro.
|
|
139
136
|
|
|
140
|
-
https://github.com/user-attachments/assets/
|
|
137
|
+
https://github.com/user-attachments/assets/aa7c2c41-b7ac-4d8b-9eb1-fd8b2feda4de
|
|
141
138
|
|
|
142
|
-
| | react-native-list |
|
|
143
|
-
| --------- |
|
|
144
|
-
| FPS | 60
|
|
145
|
-
| Memory | 86.
|
|
146
|
-
| Total CPU |
|
|
147
|
-
| Can blank | no |
|
|
139
|
+
| | Legend List | react-native-list |
|
|
140
|
+
| --------- | ----------- | ----------------- |
|
|
141
|
+
| FPS | 60 | 60 |
|
|
142
|
+
| Memory | 86.89mb | 86.88mb |
|
|
143
|
+
| Total CPU | 144.69% | 185.93% |
|
|
144
|
+
| Can blank | yes | no |
|
|
148
145
|
|
|
149
146
|
Performance difference is best described as this:
|
|
150
147
|
|
|
@@ -155,9 +152,10 @@ Performance difference is best described as this:
|
|
|
155
152
|
|
|
156
153
|
- For iOS when using dynamically sized items, try to use `iosConfig.estimatedItemSize` to roughly specify how many items will be visible in the view port. This can help a lot with performance.
|
|
157
154
|
- When specifying sizes for items use `useLinearListLayout({})` inset configs. Avoid setting a width in the styles that exceed the actual available view port width.
|
|
155
|
+
- If you can, always provide item sizes upfront. With that the performance will be unbeatable.
|
|
158
156
|
- In your item render function, when you have no item data yet it is tempting to return early with `null` or just an empty `<View />`. However, this is super bad for performance. There are two phases for native lists. First is view creation, where you're expected to create the view hierarchy for your item - just not with any data yet (so that any data could be bind to it). The second phase is actually binding data to the view. This will result in a simple "update props" operation on the native side instead of needing to create a new view hierarchy. Example:
|
|
159
157
|
|
|
160
|
-
Bad
|
|
158
|
+
❌ **Bad**:
|
|
161
159
|
|
|
162
160
|
```jsx
|
|
163
161
|
renderItemWorklet: ({ item }) => {
|
|
@@ -171,7 +169,7 @@ renderItemWorklet: ({ item }) => {
|
|
|
171
169
|
)
|
|
172
170
|
```
|
|
173
171
|
|
|
174
|
-
Good
|
|
172
|
+
✅ **Good**:
|
|
175
173
|
|
|
176
174
|
```jsx
|
|
177
175
|
renderItemWorklet: ({ item }) => {
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/* eslint-disable @react-native/no-deep-imports, @typescript-eslint/no-unused-vars, no-dupe-keys, prettier/prettier */
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
function nativeLog(...args) {
|
|
4
|
+
global._log?.('[ReactFabricMirror] ' +
|
|
5
|
+
args
|
|
6
|
+
.map((a) => {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.stringify(a);
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
return '<failed to parse> ' + String(a);
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
.join(' '));
|
|
15
|
+
}
|
|
16
|
+
global.log = nativeLog;
|
|
17
|
+
const Reconciler = require('react-reconciler');
|
|
18
|
+
// FabricUIManager is basically a wrapper around global.nativeFabricUIManager
|
|
19
|
+
// but caches each function to avoid recreating a jsi::HostFunction on each call.
|
|
20
|
+
const { getFabricUIManager, } = require('react-native/Libraries/ReactNative/FabricUIManager');
|
|
21
|
+
const uiManager = getFabricUIManager();
|
|
22
|
+
const { create: createAttributePayload, diff: diffAttributePayloads, } = require('react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload');
|
|
23
|
+
const ReactNativeViewConfigRegistry = require('react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry');
|
|
24
|
+
global.rootHostContext = {};
|
|
25
|
+
global.childHostContext = {};
|
|
26
|
+
const { NoEventPriority, DefaultEventPriority, DiscreteEventPriority, ContinuousEventPriority, IdleEventPriority, } = require('react-reconciler/constants');
|
|
27
|
+
global.currentUpdatePriority = NoEventPriority;
|
|
28
|
+
global.rootInstance = {
|
|
29
|
+
containerTag: 3,
|
|
30
|
+
publicInstance: null,
|
|
31
|
+
};
|
|
32
|
+
const { getPublicInstance } = require('../shims/react-fiber-config-fabric.js');
|
|
33
|
+
//#region Event handling
|
|
34
|
+
const EventPluginUtilsModule = require('../../third_party/react/packages/react-native-renderer/src/legacy-events/EventPluginUtils');
|
|
35
|
+
const { setComponentTree } = EventPluginUtilsModule;
|
|
36
|
+
const { injectEventPluginOrder, injectEventPluginsByName, plugins: legacyPlugins, } = require('../../third_party/react/packages/react-native-renderer/src/legacy-events/EventPluginRegistry');
|
|
37
|
+
const ResponderEventPluginModule = require('../../third_party/react/packages/react-native-renderer/src/legacy-events/ResponderEventPlugin');
|
|
38
|
+
const ReactNativeEventPluginOrderModule = require('../../third_party/react/packages/react-native-renderer/src/ReactNativeEventPluginOrder');
|
|
39
|
+
const ReactFabricGlobalResponderHandlerModule = require('../../third_party/react/packages/react-native-renderer/src/ReactFabricGlobalResponderHandler');
|
|
40
|
+
const SyntheticEventModule = require('../../third_party/react/packages/react-native-renderer/src/legacy-events/SyntheticEvent');
|
|
41
|
+
const accumulateIntoModule = require('../../third_party/react/packages/react-native-renderer/src/legacy-events/accumulateInto');
|
|
42
|
+
const forEachAccumulatedModule = require('../../third_party/react/packages/react-native-renderer/src/legacy-events/forEachAccumulated');
|
|
43
|
+
const { batchedUpdates, } = require('../../third_party/react/packages/react-native-renderer/src/legacy-events/ReactGenericBatching');
|
|
44
|
+
const { runEventsInBatch, } = require('../../third_party/react/packages/react-native-renderer/src/legacy-events/EventBatching');
|
|
45
|
+
const { HostComponent } = require('react-reconciler/src/ReactWorkTags');
|
|
46
|
+
const ResponderEventPlugin = ResponderEventPluginModule.default ?? ResponderEventPluginModule;
|
|
47
|
+
const ReactNativeEventPluginOrder = ReactNativeEventPluginOrderModule.default ?? ReactNativeEventPluginOrderModule;
|
|
48
|
+
const ReactFabricGlobalResponderHandler = ReactFabricGlobalResponderHandlerModule.default ??
|
|
49
|
+
ReactFabricGlobalResponderHandlerModule;
|
|
50
|
+
const SyntheticEvent = SyntheticEventModule.default ?? SyntheticEventModule;
|
|
51
|
+
const accumulateInto = accumulateIntoModule.default ?? accumulateIntoModule;
|
|
52
|
+
const forEachAccumulated = forEachAccumulatedModule.default ?? forEachAccumulatedModule;
|
|
53
|
+
const { customBubblingEventTypes, customDirectEventTypes } = ReactNativeViewConfigRegistry;
|
|
54
|
+
function getParent(inst) {
|
|
55
|
+
do {
|
|
56
|
+
inst = inst.return;
|
|
57
|
+
} while (inst && inst.tag !== HostComponent);
|
|
58
|
+
return inst || null;
|
|
59
|
+
}
|
|
60
|
+
function traverseTwoPhase(inst, fn, arg, skipBubbling) {
|
|
61
|
+
const path = [];
|
|
62
|
+
while (inst) {
|
|
63
|
+
path.push(inst);
|
|
64
|
+
inst = getParent(inst);
|
|
65
|
+
}
|
|
66
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
67
|
+
fn(path[i], 'captured', arg);
|
|
68
|
+
}
|
|
69
|
+
if (skipBubbling) {
|
|
70
|
+
fn(path[0], 'bubbled', arg);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
for (let i = 0; i < path.length; i++) {
|
|
74
|
+
fn(path[i], 'bubbled', arg);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function getListener(inst, registrationName) {
|
|
79
|
+
const stateNode = inst.stateNode;
|
|
80
|
+
if (stateNode == null) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const props = EventPluginUtilsModule.getFiberCurrentPropsFromNode(stateNode);
|
|
84
|
+
if (props == null) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const listener = props[registrationName];
|
|
88
|
+
if (listener != null && typeof listener !== 'function') {
|
|
89
|
+
throw new Error(`Expected \`${registrationName}\` listener to be a function, got \`${typeof listener}\`.`);
|
|
90
|
+
}
|
|
91
|
+
return listener;
|
|
92
|
+
}
|
|
93
|
+
function listenerAtPhase(inst, event, propagationPhase) {
|
|
94
|
+
const registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
|
|
95
|
+
return getListener(inst, registrationName);
|
|
96
|
+
}
|
|
97
|
+
function accumulateDirectionalDispatches(inst, phase, event) {
|
|
98
|
+
const listener = listenerAtPhase(inst, event, phase);
|
|
99
|
+
if (listener) {
|
|
100
|
+
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
|
|
101
|
+
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function accumulateTwoPhaseDispatchesSingle(event) {
|
|
105
|
+
if (event && event.dispatchConfig.phasedRegistrationNames) {
|
|
106
|
+
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event, false);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function accumulateTwoPhaseDispatches(events) {
|
|
110
|
+
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
|
|
111
|
+
}
|
|
112
|
+
function accumulateCapturePhaseDispatches(event) {
|
|
113
|
+
if (event && event.dispatchConfig.phasedRegistrationNames) {
|
|
114
|
+
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event, true);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function accumulateDispatches(inst, _ignoredDirection, event) {
|
|
118
|
+
if (inst && event && event.dispatchConfig.registrationName) {
|
|
119
|
+
const registrationName = event.dispatchConfig.registrationName;
|
|
120
|
+
const listener = getListener(inst, registrationName);
|
|
121
|
+
if (listener) {
|
|
122
|
+
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
|
|
123
|
+
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function accumulateDirectDispatchesSingle(event) {
|
|
128
|
+
if (event && event.dispatchConfig.registrationName) {
|
|
129
|
+
accumulateDispatches(event._targetInst, null, event);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function accumulateDirectDispatches(events) {
|
|
133
|
+
forEachAccumulated(events, accumulateDirectDispatchesSingle);
|
|
134
|
+
}
|
|
135
|
+
const ReactNativeBridgeEventPlugin = {
|
|
136
|
+
eventTypes: {},
|
|
137
|
+
extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
|
|
138
|
+
if (targetInst == null) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const bubbleDispatchConfig = customBubblingEventTypes[topLevelType];
|
|
142
|
+
const directDispatchConfig = customDirectEventTypes[topLevelType];
|
|
143
|
+
if (!bubbleDispatchConfig && !directDispatchConfig) {
|
|
144
|
+
throw new Error(`Unsupported top level event type "${topLevelType}" dispatched`);
|
|
145
|
+
}
|
|
146
|
+
const event = SyntheticEvent.getPooled(bubbleDispatchConfig || directDispatchConfig, targetInst, nativeEvent, nativeEventTarget);
|
|
147
|
+
if (bubbleDispatchConfig) {
|
|
148
|
+
const skipBubbling = event != null &&
|
|
149
|
+
event.dispatchConfig.phasedRegistrationNames != null &&
|
|
150
|
+
event.dispatchConfig.phasedRegistrationNames.skipBubbling;
|
|
151
|
+
if (skipBubbling) {
|
|
152
|
+
accumulateCapturePhaseDispatches(event);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
accumulateTwoPhaseDispatches(event);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (directDispatchConfig) {
|
|
159
|
+
accumulateDirectDispatches(event);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return event;
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
function extractPluginEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
|
|
168
|
+
let events = null;
|
|
169
|
+
for (let i = 0; i < legacyPlugins.length; i++) {
|
|
170
|
+
const plugin = legacyPlugins[i];
|
|
171
|
+
if (plugin) {
|
|
172
|
+
const extractedEvents = plugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
|
|
173
|
+
if (extractedEvents) {
|
|
174
|
+
events = accumulateInto(events, extractedEvents);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return events;
|
|
179
|
+
}
|
|
180
|
+
function runExtractedPluginEventsInBatch(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
|
|
181
|
+
const events = extractPluginEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
|
|
182
|
+
runEventsInBatch(events);
|
|
183
|
+
}
|
|
184
|
+
function ensureLegacyEventPluginsInjected() {
|
|
185
|
+
try {
|
|
186
|
+
injectEventPluginOrder(ReactNativeEventPluginOrder);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
// The plugin order can only be injected once for a given registry instance.
|
|
190
|
+
if (!String(error).includes('Cannot inject event plugin ordering more than once')) {
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
injectEventPluginsByName({
|
|
195
|
+
ResponderEventPlugin,
|
|
196
|
+
ReactNativeBridgeEventPlugin,
|
|
197
|
+
});
|
|
198
|
+
setComponentTree(
|
|
199
|
+
// Equivalent to ReactFabricComponentTree.getFiberCurrentPropsFromNode
|
|
200
|
+
(instance) => instance?.canonical?.currentProps ?? null,
|
|
201
|
+
// Equivalent to ReactFabricComponentTree.getInstanceFromNode
|
|
202
|
+
(node) => {
|
|
203
|
+
if (node?.canonical != null &&
|
|
204
|
+
node.canonical.internalInstanceHandle != null) {
|
|
205
|
+
return node.canonical.internalInstanceHandle;
|
|
206
|
+
}
|
|
207
|
+
return node ?? null;
|
|
208
|
+
},
|
|
209
|
+
// Equivalent to ReactFabricComponentTree.getNodeFromInstance
|
|
210
|
+
(fiber) => {
|
|
211
|
+
const publicInstance = getPublicInstance(fiber.stateNode);
|
|
212
|
+
if (publicInstance == null) {
|
|
213
|
+
throw new Error('Could not find host instance from fiber');
|
|
214
|
+
}
|
|
215
|
+
return publicInstance;
|
|
216
|
+
});
|
|
217
|
+
ResponderEventPlugin.injection.injectGlobalResponderHandler(ReactFabricGlobalResponderHandler);
|
|
218
|
+
}
|
|
219
|
+
ensureLegacyEventPluginsInjected();
|
|
220
|
+
function dispatchEvent(target, topLevelType, nativeEvent) {
|
|
221
|
+
const targetFiber = target;
|
|
222
|
+
let eventTarget = null;
|
|
223
|
+
if (targetFiber != null) {
|
|
224
|
+
const stateNode = targetFiber.stateNode;
|
|
225
|
+
if (stateNode != null) {
|
|
226
|
+
eventTarget = getPublicInstance(stateNode);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
batchedUpdates(() => {
|
|
230
|
+
runExtractedPluginEventsInBatch(topLevelType, targetFiber, nativeEvent, eventTarget);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// This will be retrieved on the native side in JSI ... hm or we call and set it?
|
|
234
|
+
global.handleEvent = dispatchEvent;
|
|
235
|
+
//#endregion
|
|
236
|
+
// Counter for uniquely identifying views.
|
|
237
|
+
// % 10 === 1 means it is a rootTag.
|
|
238
|
+
// % 2 === 0 means it is a Fabric tag.
|
|
239
|
+
// This means that they never overlap.
|
|
240
|
+
global.nextReactTag = 200_000_000;
|
|
241
|
+
// ^ Why is this number so high?
|
|
242
|
+
// We share the ReactInstance (ie we don't create a seperate one)
|
|
243
|
+
// That means our surface shares the RCTComponentViewRegistry on iOS with the main React Native renderer.
|
|
244
|
+
// When creating new views its checking if the tag already exists, meaning we can't use the same tags that react is using. Bummer!\
|
|
245
|
+
// TODO: find a better solution for this
|
|
246
|
+
const HostConfig = {
|
|
247
|
+
now: performance.now,
|
|
248
|
+
getRootHostContext(rootContainerInstance) {
|
|
249
|
+
return global.rootHostContext;
|
|
250
|
+
},
|
|
251
|
+
getChildHostContext() {
|
|
252
|
+
return global.childHostContext;
|
|
253
|
+
},
|
|
254
|
+
// prepareForCommit(containerInfo) {},
|
|
255
|
+
// resetAfterCommit(containerInfo) {},
|
|
256
|
+
supportsPersistence: true,
|
|
257
|
+
createInstance: (type, props, rootContainerInstance, _currentHostContext, workInProgress) => {
|
|
258
|
+
const tag = global.nextReactTag;
|
|
259
|
+
global.nextReactTag += 2;
|
|
260
|
+
const viewConfig = ReactNativeViewConfigRegistry.get(type);
|
|
261
|
+
const updatePayload = createAttributePayload(props, viewConfig.validAttributes);
|
|
262
|
+
let node;
|
|
263
|
+
try {
|
|
264
|
+
nativeLog('[createInstance] calling createNode with type=', type, 'tag=', tag);
|
|
265
|
+
nativeLog('[createInstance] props=', updatePayload);
|
|
266
|
+
node = uiManager.createNode(tag, // reactTag
|
|
267
|
+
viewConfig.uiViewClassName, // viewName
|
|
268
|
+
rootContainerInstance.containerTag, // rootTag
|
|
269
|
+
updatePayload, // props
|
|
270
|
+
workInProgress // internalInstanceHandle
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
catch (e) {
|
|
274
|
+
nativeLog('[createInstance] ERROR in createNode:', e.message || String(e));
|
|
275
|
+
nativeLog('Stack:', new Error().stack);
|
|
276
|
+
throw e;
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
node: node,
|
|
280
|
+
canonical: {
|
|
281
|
+
nativeTag: tag,
|
|
282
|
+
viewConfig, // TODO: is this needed? for what
|
|
283
|
+
currentProps: props, // funny, react is passing here props instead of updatePayload, is this okay?
|
|
284
|
+
internalInstanceHandle: workInProgress,
|
|
285
|
+
publicInstance: null,
|
|
286
|
+
publicRootInstance: rootContainerInstance.publicInstance,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
finalizeInitialChildren(parentInstance, type, props, hostContext) {
|
|
291
|
+
nativeLog('[finalizeInitialChildren]');
|
|
292
|
+
return false;
|
|
293
|
+
},
|
|
294
|
+
cloneInstance(instance, type, oldProps, newProps, keepChildren, newChildSet) {
|
|
295
|
+
nativeLog('[cloneInstance] tag=', instance.canonical.nativeTag);
|
|
296
|
+
const viewConfig = instance.canonical.viewConfig;
|
|
297
|
+
const updatePayload = diffAttributePayloads(oldProps, newProps, viewConfig.validAttributes);
|
|
298
|
+
nativeLog('[cloneInstance] updatePayload=', updatePayload);
|
|
299
|
+
// TODO: If the event handlers have changed, we need to update the current props
|
|
300
|
+
// in the commit phase but there is no host config hook to do it yet.
|
|
301
|
+
// So instead we hack it by updating it in the render phase.
|
|
302
|
+
instance.canonical.currentProps = newProps;
|
|
303
|
+
const node = instance.node;
|
|
304
|
+
let clone;
|
|
305
|
+
if (keepChildren) {
|
|
306
|
+
if (updatePayload !== null) {
|
|
307
|
+
clone = uiManager.cloneNodeWithNewProps(node, updatePayload);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
// No changes
|
|
311
|
+
return instance;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// If passChildrenWhenCloningPersistedNodes is enabled, children will be non-null
|
|
316
|
+
if (newChildSet != null) {
|
|
317
|
+
if (updatePayload !== null) {
|
|
318
|
+
clone = uiManager.cloneNodeWithNewChildrenAndProps(node, newChildSet, updatePayload);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
clone = uiManager.cloneNodeWithNewChildren(node, newChildSet);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
if (updatePayload !== null) {
|
|
326
|
+
clone = uiManager.cloneNodeWithNewChildrenAndProps(node, updatePayload);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
clone = uiManager.cloneNodeWithNewChildren(node);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
node: clone,
|
|
335
|
+
canonical: instance.canonical,
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
createTextInstance(text, rootContainerInstance, hostContext, internalInstanceHandle) {
|
|
339
|
+
const tag = global.nextReactTag;
|
|
340
|
+
global.nextReactTag += 2;
|
|
341
|
+
const node = uiManager.createNode(tag, // reactTag
|
|
342
|
+
'RCTRawText', // viewName
|
|
343
|
+
rootContainerInstance.containerTag, // rootTag
|
|
344
|
+
{ text: text }, // props
|
|
345
|
+
internalInstanceHandle // instance handle
|
|
346
|
+
);
|
|
347
|
+
return {
|
|
348
|
+
node: node,
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
createContainerChildSet() {
|
|
352
|
+
nativeLog('[createContainerChildSet]');
|
|
353
|
+
return uiManager.createChildSet();
|
|
354
|
+
},
|
|
355
|
+
appendChildToContainerChildSet(childSet, child) {
|
|
356
|
+
nativeLog('[appendChildToContainerChildSet]');
|
|
357
|
+
uiManager.appendChildToSet(childSet, child.node);
|
|
358
|
+
},
|
|
359
|
+
finalizeContainerChildren(container, newChildren) {
|
|
360
|
+
// Noop - children will be replaced in replaceContainerChildren
|
|
361
|
+
nativeLog('[finalizeContainerChildren]');
|
|
362
|
+
},
|
|
363
|
+
appendInitialChild(parentInstance, child) {
|
|
364
|
+
nativeLog('[appendInitialChild]');
|
|
365
|
+
uiManager.appendChild(parentInstance.node, child.node);
|
|
366
|
+
},
|
|
367
|
+
replaceContainerChildren(container, newChildren) {
|
|
368
|
+
nativeLog('[replaceContainerChildren]');
|
|
369
|
+
uiManager.completeRoot(container.containerTag, newChildren);
|
|
370
|
+
},
|
|
371
|
+
// TODO: hm, this could get problematic, to share event priorities between the fabric ui manager.
|
|
372
|
+
// Maybe we need to create a clone of the ui manager? the important thing is that all view managers are resolved when we do so.
|
|
373
|
+
// However that would concern me if commitHooks from reanimated would work.
|
|
374
|
+
setCurrentUpdatePriority(priority) {
|
|
375
|
+
global.currentUpdatePriority = priority;
|
|
376
|
+
},
|
|
377
|
+
getCurrentUpdatePriority() {
|
|
378
|
+
return global.currentUpdatePriority;
|
|
379
|
+
},
|
|
380
|
+
resolveUpdatePriority() {
|
|
381
|
+
if (global.currentUpdatePriority !== NoEventPriority) {
|
|
382
|
+
return global.currentUpdatePriority;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
return DefaultEventPriority;
|
|
386
|
+
}
|
|
387
|
+
// TODO:
|
|
388
|
+
/* const currentEventPriority = fabricGetCurrentEventPriority
|
|
389
|
+
? fabricGetCurrentEventPriority()
|
|
390
|
+
: null;
|
|
391
|
+
|
|
392
|
+
if (currentEventPriority != null) {
|
|
393
|
+
switch (currentEventPriority) {
|
|
394
|
+
case FabricDiscretePriority:
|
|
395
|
+
return DiscreteEventPriority;
|
|
396
|
+
case FabricContinuousPriority:
|
|
397
|
+
return ContinuousEventPriority;
|
|
398
|
+
case FabricIdlePriority:
|
|
399
|
+
return IdleEventPriority;
|
|
400
|
+
case FabricDefaultPriority:
|
|
401
|
+
default:
|
|
402
|
+
return DefaultEventPriority;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return DefaultEventPriority; */
|
|
407
|
+
},
|
|
408
|
+
getPublicInstance(instance) {
|
|
409
|
+
return getPublicInstance(instance);
|
|
410
|
+
},
|
|
411
|
+
// getPublicTextInstance(textInstance, internalInstanceHandle) {
|
|
412
|
+
// if (textInstance.publicInstance == null) {
|
|
413
|
+
// textInstance.publicInstance = createPublicTextInstance(
|
|
414
|
+
// internalInstanceHandle
|
|
415
|
+
// )
|
|
416
|
+
// }
|
|
417
|
+
// return textInstance.publicInstance
|
|
418
|
+
// },
|
|
419
|
+
// getPublicInstanceFromInternalInstanceHandle(internalInstanceHandle) {
|
|
420
|
+
// const instance = internalInstanceHandle.stateNode
|
|
421
|
+
// // React resets all the fields in the fiber when the component is unmounted
|
|
422
|
+
// // to prevent memory leaks.
|
|
423
|
+
// if (instance == null) {
|
|
424
|
+
// return null
|
|
425
|
+
// }
|
|
426
|
+
// if (internalInstanceHandle.tag === HostText) {
|
|
427
|
+
// const textInstance = instance
|
|
428
|
+
// return this.getPublicTextInstance(textInstance, internalInstanceHandle)
|
|
429
|
+
// }
|
|
430
|
+
// const elementInstance = internalInstanceHandle.stateNode
|
|
431
|
+
// return this.getPublicInstance(elementInstance)
|
|
432
|
+
// },
|
|
433
|
+
prepareForCommit(containerInfo) {
|
|
434
|
+
return null;
|
|
435
|
+
},
|
|
436
|
+
resetAfterCommit(containerInfo) { },
|
|
437
|
+
trackSchedulerEvent() { },
|
|
438
|
+
resolveEventType() {
|
|
439
|
+
return null;
|
|
440
|
+
},
|
|
441
|
+
resolveEventTimeStamp() {
|
|
442
|
+
return -1.1;
|
|
443
|
+
},
|
|
444
|
+
shouldAttemptEagerTransition() {
|
|
445
|
+
return false;
|
|
446
|
+
},
|
|
447
|
+
shouldSetTextContent(type, props) {
|
|
448
|
+
return false;
|
|
449
|
+
},
|
|
450
|
+
// TODO: microtask scheduling, should work with worklets on the UI thread i believe! not sure though if even RN implements this?
|
|
451
|
+
supportsMicrotasks: false,
|
|
452
|
+
detachDeletedInstance(node) { },
|
|
453
|
+
beforeActiveInstanceBlur(internalInstanceHandle) {
|
|
454
|
+
// noop
|
|
455
|
+
},
|
|
456
|
+
afterActiveInstanceBlur() {
|
|
457
|
+
// noop
|
|
458
|
+
},
|
|
459
|
+
preparePortalMount(portalInstance) {
|
|
460
|
+
// noop
|
|
461
|
+
},
|
|
462
|
+
detachDeletedInstance(node) {
|
|
463
|
+
// noop
|
|
464
|
+
},
|
|
465
|
+
requestPostPaintCallback(callback) {
|
|
466
|
+
// noop
|
|
467
|
+
},
|
|
468
|
+
maySuspendCommit(type, props) {
|
|
469
|
+
return false;
|
|
470
|
+
},
|
|
471
|
+
maySuspendCommitOnUpdate(type, oldProps, newProps) {
|
|
472
|
+
return false;
|
|
473
|
+
},
|
|
474
|
+
maySuspendCommitInSyncRender(type, props) {
|
|
475
|
+
return false;
|
|
476
|
+
},
|
|
477
|
+
preloadInstance(instance, type, props) {
|
|
478
|
+
return true;
|
|
479
|
+
},
|
|
480
|
+
startSuspendingCommit() {
|
|
481
|
+
return null;
|
|
482
|
+
},
|
|
483
|
+
suspendInstance(state, instance, type, props) { },
|
|
484
|
+
suspendOnActiveViewTransition(state, container) { },
|
|
485
|
+
waitForCommitToBeReady(state, timeoutOffset) {
|
|
486
|
+
return null;
|
|
487
|
+
},
|
|
488
|
+
getSuspendedCommitReason(state, rootContainer) {
|
|
489
|
+
return null;
|
|
490
|
+
},
|
|
491
|
+
isPrimaryRenderer: false,
|
|
492
|
+
};
|
|
493
|
+
const Renderer = Reconciler(HostConfig);
|
|
494
|
+
global.React = require('react');
|
|
495
|
+
function reactRender(element, callback) {
|
|
496
|
+
if (!global.rootContainer) {
|
|
497
|
+
global.rootContainer = Renderer.createContainer(global.rootInstance, 0, // concurrentRoot ? 1 : 0
|
|
498
|
+
null, false, null, 'ui-renderer', function onUncaughtError(error, info) {
|
|
499
|
+
nativeLog('[Error][ReactFabricMirror] Uncaught error in React renderer: ', error, info);
|
|
500
|
+
}, function onCaughtError(error, info) {
|
|
501
|
+
nativeLog('[Error][ReactFabricMirror] Caught error in React renderer: ', error, info);
|
|
502
|
+
}, function onRecoverableError(error, info) {
|
|
503
|
+
nativeLog('[Error][ReactFabricMirror] Recoverable error in React renderer: ', error, info);
|
|
504
|
+
}, function nativeOnDefaultTransitionIndicator() {
|
|
505
|
+
// Native doesn't have a default indicator.
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
// updateContainerSync + flushSyncWork is making the renderer work immediately/blocking/…sync
|
|
509
|
+
Renderer.updateContainerSync(element, global.rootContainer, null, callback);
|
|
510
|
+
// Renderer.flushPassiveEffects();
|
|
511
|
+
Renderer.flushSyncWork();
|
|
512
|
+
nativeLog('[ReactFabricMirror] updateContainer finished');
|
|
513
|
+
}
|
|
514
|
+
nativeLog('[ReactFabricMirror] ReactFabricMirror initialized');
|
|
515
|
+
export { nativeLog, reactRender };
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { uiListModule } from './UiListModule';
|
|
2
|
+
import { uiManagerHelper } from './renderer/fabric/UiManagerHelper';
|
|
3
|
+
import { List } from './views/List';
|
|
4
|
+
export { ViewHolder } from './specs/ViewHolder.nitro';
|
|
5
|
+
export { IOSWorkletsModuleProxyHolder } from './specs/IOSWorkletsModuleProxyHolder.nitro';
|
|
6
|
+
export type { NativeListItem } from './specs/UiListView.nitro';
|
|
7
|
+
export type { ListItemSize, ListKey, ListProps, ListRef, ListRenderer, } from './views/List';
|
|
8
|
+
export { List, uiListModule, uiManagerHelper };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { scheduleOnUI } from 'react-native-worklets';
|
|
2
|
+
import { uiListModule } from './UiListModule';
|
|
3
|
+
import { uiManagerHelper } from './renderer/fabric/UiManagerHelper';
|
|
4
|
+
import { List } from './views/List';
|
|
5
|
+
import { Platform } from 'react-native';
|
|
6
|
+
import { getReactFabricRenderer } from './renderer/react/ReactFabricRenderer';
|
|
7
|
+
const boxed = uiListModule;
|
|
8
|
+
const nativeFabricUIManager = globalThis.nativeFabricUIManager;
|
|
9
|
+
function setup() {
|
|
10
|
+
// TODO: ask SWM if they can remove their JS thread checks, then we could just access this from the UI thread.
|
|
11
|
+
const iosWorkletsModuleHolder = Platform.OS === 'ios' ? uiListModule.iosGetWorkletsModule() : null;
|
|
12
|
+
scheduleOnUI(() => {
|
|
13
|
+
'worklet';
|
|
14
|
+
globalThis.nativeFabricUIManager = nativeFabricUIManager;
|
|
15
|
+
boxed.setupExternalSurface(iosWorkletsModuleHolder);
|
|
16
|
+
// This will setup the react instance on the UI runtime:
|
|
17
|
+
getReactFabricRenderer();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
setup();
|
|
21
|
+
export { List, uiListModule, uiManagerHelper };
|