Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion #1

Open
Lelelo1 opened this issue Feb 14, 2021 · 20 comments
Open

Discussion #1

Lelelo1 opened this issue Feb 14, 2021 · 20 comments

Comments

@Lelelo1
Copy link

Lelelo1 commented Feb 14, 2021

So know that RCTView etc can be instantiated in nativescript. I think the a next step could be to make a adapted rn version with altered ReactCommon or Libraries/Components?
All rn components seem to use codegenNativeComponent, codegen - there might be some way to have it it use the nativescript bindings there instead?

@shirakaba
Copy link
Member

Next steps

I think the a next step could be to make a adapted rn version with altered ReactCommon or Libraries/Components?

For one thing, I'm not clear what's useful to us in ReactCommon.

For another thing, we're still very far away from doing anything useful with views, so we should continue to focus on solving that part. See the interface for RCTView:

declare class RCTView extends UIView {

	static alloc(): RCTView; // inherited from NSObject

	static appearance(): RCTView; // inherited from UIAppearance

	static appearanceForTraitCollection(trait: UITraitCollection): RCTView; // inherited from UIAppearance

	static appearanceForTraitCollectionWhenContainedIn(trait: UITraitCollection, ContainerClass: typeof NSObject): RCTView; // inherited from UIAppearance

	static appearanceForTraitCollectionWhenContainedInInstancesOfClasses(trait: UITraitCollection, containerTypes: NSArray<typeof NSObject> | typeof NSObject[]): RCTView; // inherited from UIAppearance

	static appearanceWhenContainedIn(ContainerClass: typeof NSObject): RCTView; // inherited from UIAppearance

	static appearanceWhenContainedInInstancesOfClasses(containerTypes: NSArray<typeof NSObject> | typeof NSObject[]): RCTView; // inherited from UIAppearance

	static autoAdjustInsetsForViewWithScrollViewUpdateOffset(parentView: UIView, scrollView: UIScrollView, updateOffset: boolean): void;

	static contentInsetsForView(curView: UIView): UIEdgeInsets;

	static new(): RCTView; // inherited from NSObject

	borderBottomColor: any;

	borderBottomEndRadius: number;

	borderBottomLeftRadius: number;

	borderBottomRightRadius: number;

	borderBottomStartRadius: number;

	borderBottomWidth: number;

	borderColor: any;

	borderEndColor: any;

	borderEndWidth: number;

	borderLeftColor: any;

	borderLeftWidth: number;

	borderRadius: number;

	borderRightColor: any;

	borderRightWidth: number;

	borderStartColor: any;

	borderStartWidth: number;

	borderStyle: RCTBorderStyle;

	borderTopColor: any;

	borderTopEndRadius: number;

	borderTopLeftRadius: number;

	borderTopRightRadius: number;

	borderTopStartRadius: number;

	borderTopWidth: number;

	borderWidth: number;

	hitTestEdgeInsets: UIEdgeInsets;

	onAccessibilityAction: (p1: NSDictionary<any, any>) => void;

	onAccessibilityEscape: (p1: NSDictionary<any, any>) => void;

	onAccessibilityTap: (p1: NSDictionary<any, any>) => void;

	onMagicTap: (p1: NSDictionary<any, any>) => void;

	pointerEvents: RCTPointerEvents;

	removeClippedSubviews: boolean;

	updateClippedSubviews(): void;
}

Note how RCTView doesn't implement any Yoga properties (e.g. alignItems, justifyContent, position, width, height, etc.). All that stuff is implemented instead by RCTShadowView:

declare class RCTShadowView extends NSObject implements RCTComponent {

	static alloc(): RCTShadowView; // inherited from NSObject

	static new(): RCTShadowView; // inherited from NSObject

	static yogaConfig(): interop.Pointer | interop.Reference<any>;

	alignContent: YGAlign;

	alignItems: YGAlign;

	alignSelf: YGAlign;

	aspectRatio: number;

	readonly availableSize: CGSize;

	readonly borderAsInsets: UIEdgeInsets;

	borderBottomWidth: number;

	borderEndWidth: number;

	borderLeftWidth: number;

	borderRightWidth: number;

	borderStartWidth: number;

	borderTopWidth: number;

	borderWidth: number;

	bottom: YGValue;

	readonly compoundInsets: UIEdgeInsets;

	readonly contentFrame: CGRect;

	direction: YGDirection;

	display: YGDisplay;

	end: YGValue;

	flex: number;

	flexBasis: YGValue;

	flexDirection: YGFlexDirection;

	flexGrow: number;

	flexShrink: number;

	flexWrap: YGWrap;

	height: YGValue;

	intrinsicContentSize: CGSize;

	justifyContent: YGJustify;

	layoutMetrics: RCTLayoutMetrics;

	left: YGValue;

	margin: YGValue;

	marginBottom: YGValue;

	marginEnd: YGValue;

	marginHorizontal: YGValue;

	marginLeft: YGValue;

	marginRight: YGValue;

	marginStart: YGValue;

	marginTop: YGValue;

	marginVertical: YGValue;

	maxHeight: YGValue;

	maxWidth: YGValue;

	minHeight: YGValue;

	minWidth: YGValue;

	newView: boolean;

	onLayout: (p1: NSDictionary<any, any>) => void;

	overflow: YGOverflow;

	padding: YGValue;

	readonly paddingAsInsets: UIEdgeInsets;

	paddingBottom: YGValue;

	paddingEnd: YGValue;

	paddingHorizontal: YGValue;

	paddingLeft: YGValue;

	paddingRight: YGValue;

	paddingStart: YGValue;

	paddingTop: YGValue;

	paddingVertical: YGValue;

	position: YGPositionType;

	right: YGValue;

	rootView: RCTRootShadowView;

	size: CGSize;

	start: YGValue;

	readonly superview: RCTShadowView;

	top: YGValue;

	viewName: string;

	width: YGValue;

	readonly yogaNode: interop.Pointer | interop.Reference<any>;

	readonly debugDescription: string; // inherited from NSObjectProtocol

	readonly description: string; // inherited from NSObjectProtocol

	readonly hash: number; // inherited from NSObjectProtocol

	readonly isProxy: boolean; // inherited from NSObjectProtocol

	reactTag: number; // inherited from RCTComponent

	rootTag: number; // inherited from RCTComponent

	readonly superclass: typeof NSObject; // inherited from NSObjectProtocol

	readonly  // inherited from NSObjectProtocol

	canHaveSubviews(): boolean;

	class(): typeof NSObject;

	clearLayout(): void;

	conformsToProtocol(aProtocol: any /* Protocol */): boolean;

	didSetProps(changedProps: NSArray<string> | string[]): void;

	didUpdateReactSubviews(): void;

	dirtyLayout(): void;

	insertReactSubviewAtIndex(subview: RCTShadowView, atIndex: number): void;

	isEqual(object: any): boolean;

	isKindOfClass(aClass: typeof NSObject): boolean;

	isMemberOfClass(aClass: typeof NSObject): boolean;

	isReactRootView(): boolean;

	isYogaLeafNode(): boolean;

	layoutSubviewsWithContext(layoutContext: RCTLayoutContext): void;

	layoutWithMetricsLayoutContext(layoutMetrics: RCTLayoutMetrics, layoutContext: RCTLayoutContext): void;

	layoutWithMinimumSizeMaximumSizeLayoutDirectionLayoutContext(minimumSize: CGSize, maximumSize: CGSize, layoutDirection: UIUserInterfaceLayoutDirection, layoutContext: RCTLayoutContext): void;

	measureLayoutRelativeToAncestor(ancestor: RCTShadowView): CGRect;

	performSelector(aSelector: string): any;

	performSelectorWithObject(aSelector: string, object: any): any;

	performSelectorWithObjectWithObject(aSelector: string, object1: any, object2: any): any;

	reactSubviews(): NSArray<RCTComponent>;

	reactSuperview(): RCTShadowView;

	reactTagAtPoint(point: CGPoint): number;

	removeReactSubview(subview: RCTShadowView): void;

	respondsToSelector(aSelector: string): boolean;

	retainCount(): number;

	self(): this;

	setLocalData(localData: NSObject): void;

	sizeThatFitsMinimumSizeMaximumSize(minimumSize: CGSize, maximumSize: CGSize): CGSize;

	viewIsDescendantOf(ancestor: RCTShadowView): boolean;
}

React Native UI architecture

In order to implement any of the React Native views as NativeScript View elements (more specifically, as extensions of LayoutBase or FlexboxLayout), we need to understand how React Native updates its UI. i.e. what is the purpose of RCTView, RCTViewManager, RCTUIManager, and RCTShadowView? Which of these are responsible for what? Do we need the bridge or not?

Background

Understanding React Native Architecture:

Create native UI component in React Native

React.js Conf 2016 - Tadeu Zagallo - Optimising React Native: Tools and Tips

image

Conclusion so far

React Native maintains a shadow UI hierarchy. i.e. For each node you add to your real UI hierarchy, it adds one to a "shadow UI hierarchy" as well, which is responsible for calculating layout. During each render, once the layout is determined in the shadow UI, React Native applies those layout changes to our real UI.

These comments in RCTShadowView.h are particularly informative:

/**
 * ShadowView tree mirrors RCT view tree. Every node is highly stateful.
 * 1. A node is in one of three lifecycles: uninitialized, computed, dirtied.
 * 1. RCTBridge may call any of the padding/margin/width/height/top/left setters. A setter would dirty
 *    the node and all of its ancestors.
 * 2. At the end of each Bridge transaction, we call layoutWithMinimumSize:maximumSize:layoutDirection:layoutContext
 *    at the root node to recursively lay out the entire hierarchy.
 * 3. If a node is "computed" and the constraint passed from above is identical to the constraint used to
 *    perform the last computation, we skip laying out the subtree entirely.
 */

NativeScript UI architecture

In order to plug these bits of React Native functionality into NativeScript, we also need to understanding how the NativeScript UI works. When does it update? How does it layout? How (and when) do the wrapper components coordinate their native views? Those sorts of things.

Background

User Interface Layout Process

Conclusion so far

NativeScript does a measure pass based on certain properties like width and height, followed by a layout pass based on certain properties like margin.

Work so far

I'm trying to create a NativeScript element called RNLayout, which is an extension of FlexboxLayout. It will own an RCTShadowView and an RCTView. Upon receiving an event from NativeScript to add/remove a child node, it will add that child node's shadow view to its own shadow view. And upon receiving layout change events from NativeScript, it will inform its own shadow view to recalculate its layout (and the layout of its children, recursively). On the measure pass, it will apply those changes to the RCTView.

Well, that's the plan, but it's seriously complex and I'm not confident I'll be able to finish it, due to lack of understanding of both React Native and NativeScript, and lack of contacts to talk to who know how either framework's systems work. But that's where I am, anyway.


All rn components seem to use codegenNativeComponent, codegen - there might be some way to have it it use the nativescript bindings there instead?

I'm not thinking that far yet. I'm not familiar enough with its role in React Native to know what we might do with it. It may or may not be redundant.

@shirakaba
Copy link
Member

shirakaba commented Feb 14, 2021

Some more end-to-end notes on the React Native rendering process, determined by reverse-engineering as far as I can tell:

Original, in Chinese: https://www.jianshu.com/p/5cc61ec04b39

English translation: https://www.programmersought.com/article/93734568742/

And lots of internals documented here, particularly on the rendering process – but again, only in Chinese. Google translation into English here.

@Lelelo1
Copy link
Author

Lelelo1 commented Feb 14, 2021

I just think that:

  • NativeScript can call React Native to say "HelloWorld"
  • Custom React Native version makes native calls in js instead of objc/java

After that we can decide depending on what we want..

<stackLayout>
    <TouchbleOpacity />
</stackLayout>

Or?

<abstractStackLayout>
    <abstractTouchableOpacity />
</abstractStackLayout>

The former is much more desirable to me. (I can put react native project into ns and get language bindings and keep on working with rn components and start right away, if I want - I can also start exploring ns components in some places)
The latter actually forces you rewrite your project.
:)

@shirakaba
Copy link
Member

shirakaba commented Feb 15, 2021

I've asked Evan Bacon if he could share his Webpack config for running React Native iOS/Android via Webpack.

If he can, then we'd have a basis for consuming the JS code of react-native. I'm then thinking that we could use NativeScript to create and manage a full React Native app (you'd use <View> and <Text> and all the other view components from React Native, but none of the UI components from NativeScript), by constructing the RCTBridge and RCTRootView in the NativeScript JS context. The latter part is relatively simple, but the feasibility of the whole idea does all depend on Evan's response.

We wouldn't be able to (easily) mix NativeScript and React Native views, and we'd only be able to support React projects (not Vue/Angular/Svelte), but with that approach, we would be able to code completely as if it were a React Native app, but with the added benefit of NativeScript's ability to call native APIs via JS.

Extra work would be needed to support things like autolinking of React Native native modules, but it would still be a huge step forwards.

@Lelelo1
Copy link
Author

Lelelo1 commented Feb 15, 2021

There is a mention of a bridgeless mode
Ideally the bridge could be disconnected. Otherwise a new dependency is created!

@shirakaba
Copy link
Member

I saw that – it's undocumented, so I wouldn't know how to use it (nor whether it's actually a completed feature or just experimental). Looks exciting, but will have to take things one step at a time.

@Lelelo1
Copy link
Author

Lelelo1 commented Feb 15, 2021

It looks incomplete to me. Also it seems createReactNativeComponentClass for instance is only used in one place. So there is no generalised initialisation of the native side - at least I can't see it now

@shirakaba
Copy link
Member

I found the project that lets you build React Native apps using Webpack (rather than Metro). It looks pretty complicated, so it won't be easy, but it does provide the foundation for adapting our NativeScript Webpack config so that a NativeScript project could directly consume React Native code:

https://github.com/callstack/haul

@Lelelo1
Copy link
Author

Lelelo1 commented Feb 15, 2021

Most components that end in native implementation seems to use codegenNativeComponent<NativeProps> being exported out of the file. And it is present 43 files. ActivityIndicator, SafeAreaView, and View all got it.

There is NativeComponentRegistry.get also.. <--

Gets a NativeComponent that can be rendered by React Native

And also:
requireNativeComponent

@Lelelo1
Copy link
Author

Lelelo1 commented Feb 15, 2021

So far, I think it seems every ui instance is being ViewConfig instances (to the js side):

export type ViewConfig = $ReadOnly<{
  Commands?: $ReadOnly<{[commandName: string]: number, ...}>,
  Constants?: $ReadOnly<{[name: string]: mixed, ...}>,
  Manager?: string,
  NativeProps?: $ReadOnly<{[propName: string]: string, ...}>,
  baseModuleName?: ?string,
  bubblingEventTypes?: $ReadOnly<{
    [eventName: string]: $ReadOnly<{
      phasedRegistrationNames: $ReadOnly<{
        captured: string,
        bubbled: string,
      }>,
    }>,
    ...,
  }>,
  directEventTypes?: $ReadOnly<{
    [eventName: string]: $ReadOnly<{
      registrationName: string,
    }>,
    ...,
  }>,
  uiViewClassName: string,
  validAttributes: AttributeConfiguration,
}>;

...Looking around in NativeComponentRegistry it seems to be the case for bridge and bridgeless.

@Lelelo1
Copy link
Author

Lelelo1 commented Feb 16, 2021

I found `const turboModuleProxy = global.__turboModuleProxy;` to be the source where the attributes are coming from - which populate the ViewConfig behind the View. And current rn seem to be using bridge for now.

I'ts NativeModules = global.nativeModuleProxy; which rn currently uses and uses bridge

Here is some information I found.
http://blog.nparashuram.com/2019/01/

@shirakaba
Copy link
Member

I've got a React Native app building using only Webpack (rather than Metro):

image

No NativeScript involved yet; it's a pure React Native project. But following this proof-of-concept, I can study its webpack config and see if there's a way to merge that into a NativeScript project's Webpack config, allowing a NativeScript project to consume React Native JS code.

@Lelelo1
Copy link
Author

Lelelo1 commented Feb 17, 2021

There is one initial get call when the importing of components are made and i'ts possible to call it directly in a rn project directly like so:

    // RCTSwitch
    // RCTView
    // RCTDatePicker
    //const view = NativeModules.UIManager.getViewManagerConfig('RCTDatePicker');

There is a mentioning of 3d party actors that a may wanting to call it:

// TODO (T45220498): Remove this.
// 3rd party libs may be calling NativeModules.UIManager.getViewManagerConfig()
// instead of UIManager.getViewManagerConfig() off UIManager.js.
// This is a workaround for now.
// $FlowFixMe

What you get is all start/default values and props, the "config".

I think these ultimately come from cpp over the bridge. I'ts also related to NativeModules['UIManager'].
For some weird reason NativeModules is not loopable even though it contains at least one key (otherwise I would defiantly want to list them)


There is export function get<T: TurboModule>(name: string): ?T in TurboModuleRgesitry.js`

[Wed Feb 17 2021 17:29:27.974] LOG naame: KeyboardObserver
[Wed Feb 17 2021 17:29:27.975] LOG I am called {}
[Wed Feb 17 2021 17:29:27.975] LOG naame: SoundManager
[Wed Feb 17 2021 17:29:27.976] LOG I am called null
[Wed Feb 17 2021 17:29:27.976] LOG naame: ImageLoader
[Wed Feb 17 2021 17:29:27.977] LOG I am called {}
[Wed Feb 17 2021 17:29:27.977] LOG mmmoo: {}
[Wed Feb 17 2021 17:29:27.977] LOG naame: StatusBarManager
[Wed Feb 17 2021 17:29:27.981] LOG I am called {"HEIGHT":48}
[Wed Feb 17 2021 17:29:27.981] LOG mmmoo: {"HEIGHT":48}

// Bridgeless mode requires TurboModules
  if (!global.RN$Bridgeless) {
    // Backward compatibility layer during migration.
    const legacyModule = NativeModules[name];
    console.log("naame: " + name);

    console.log("I am called " + (JSON.stringify(legacyModule)));
    for (var key in NativeModules) {
      console.log(key);
    }

    if (legacyModule != null) {
      return ((legacyModule: any): T);
    }
  }

None of this is called again when trigger a rerender (changing color of View).

@Lelelo1
Copy link
Author

Lelelo1 commented Feb 18, 2021

It probably occurs sometime during "App starts" event (looking at the diagram). I think the ui templates are retrieved (the above). I don't think this initial call is one of the call that "Return dimensions"

I still have to find js call over bridge, which should occur every render.. and log properties. (But is this a get call made from the bridge maybe?)

@Lelelo1
Copy link
Author

Lelelo1 commented Feb 20, 2021

.. Trying to find the instantiation by looking at the rendering. There are ReactNativeRenderer and ReactFabric for rendering.

if (fabric) {
    require('../Renderer/shims/ReactFabric').render(renderable, rootTag);
  } else {
    require('../Renderer/shims/ReactNative').render(renderable, rootTag); //<-- currently active
  }

The following blog mentions "Fabric" as being part of the rearchitecting: https://formidable.com/blog/2019/fabric-turbomodules-part-3/. It does not seem to be in production yet


Concerning the "Turbo Modules", TurboModuleRegistry.get is used but it contains the legacy call I posted above that is being used

@Lelelo1
Copy link
Author

Lelelo1 commented Feb 20, 2021

It seems the rendering makes calls to ReactNativePrivateInterface.UIManager. There is a cpp one - under "Fabric" folder. So I think that it is under construction.
There is a objective c implementation and java implementation. I think it should be possible to create a custom nativescript view manager that calls com.facebook.* components in js.

There is a chance it can work together on it's own though once it's possible to call rn in ns as we said on slack

@Lelelo1
Copy link
Author

Lelelo1 commented Feb 28, 2021

I think we should focus on calling:

import { Alert } from 'react-native';
function onTap(args: EventData) => {
        console.log('tap');
        Alert.alert('title', 'message'); // < --
}

..Or just something that might not be using bridge. Just to get that working first.

@shirakaba
Copy link
Member

All JS APIs involving native functionality use the bridge.

I can’t think of many that don’t involve native functionality. Maybe some StyleSheet APIs?

@Lelelo1
Copy link
Author

Lelelo1 commented Feb 28, 2021

My own idea involve trying to get the components out of rn, in some nice way by maybe replacing/overwriting modules - like the UIManager.

But I don't understand why you expect native parts to work when the non native parts don't work. (I assume the Fatal exception loaders occurs everywhere). Or have you turned off hmr and used something in rn - before preceding trying to get all working at once?

I stopped investigating the UIMangar amongst other because I wanted to make sure I could call something (and then continue on by experimenting)

@shirakaba
Copy link
Member

But I don't understand why you expect native parts to work when the non native parts don't work. (I assume the Fatal exception loaders occurs everywhere). Or have you turned off hmr and used something in rn - before preceding trying to get all working at once.

I believe I was able to import and log React Native’s View module on the haul branch, as long as I didn’t set a custom AppDelegate. Beyond that, I didn’t try calling any of React Native’s JS APIs, however.

I honestly might be misremembering, however. Was focusing more on getting RCTBridge to work, because at that point I’d expect everything to work (because we’re bundling code in mostly the same way that Haul does for pure React Native projects).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants