shirakaba / react-nativescript

React renderer for NativeScript
https://react-nativescript.netlify.com
MIT License
280 stars 14 forks source link

Accessing the native component? #31

Closed Lelelo1 closed 5 years ago

Lelelo1 commented 5 years ago

I am trying to access the native component of button.ios/textfield.ios etc. But layouts and controls are all undefined in the componentDidMount method.

export default class ComponentExample extends React.Component {
    stackLayoutRef = React.createRef<StackLayout>();
    buttonRef = React.createRef<Button>();

    componentDidMount() {
        console.log("did mount succesfully");
        // both native ios components are undefined...
        console.log("stackLayout " + this.stackLayoutRef.current.ios);
        console.log("and button: " + this.buttonRef.current.ios);
    }

    render() {
        return (
            <$StackLayout ref={this.stackLayoutRef}>
                <$Label text={"This is an example"}/>
                <$TextField text="input"/>
                <$Button ref={this.buttonRef} text="test" backgroundColor={new Color('purple')}/>
            </$StackLayout>
        )
    }
}

Both Frame and Page ios properties can be accessed however.

I have also tried to:

render() {
        return (
            <$StackLayout ref={(ref) => {
                // Intending on assigning ref to instance variable to use inside componentDidMount
                console.log("stackRef: " + ref); // <-- does print: 'stackRef: StackLayout(2)'
                // crash
            }}>
                <$Button text="test" backgroundColor={new Color('purple')}/>
            </$StackLayout>
        )
    } 

But then it crash with the following error: "*** Terminating app due to uncaught exception 'NativeScript encountered a fatal error: TypeError: undefined is not an object (evaluating 'node.on') at". And I get the same error trying it with the Button as well - coming from simply assigning ref with a anonymous method (ref) => { };

How can I for example access the UIButton from the Button in react-nativescript?

shirakaba commented 5 years ago

Thanks for the Issue filing.

Your code there is correct – it's absolutely the right way to access UIButton. However, I think there's a race condition with ReactNativeScript.render() (same as there would be with ReactNative.render() and ReactDOM.render()) – which is part of the startup function and also can be a part of navigation actions – that is causing these refs to not be populated by the time the component mounts. The problem is that it's so hard to reproduce. I'm trying to build a real-world app to learn more about a good pattern for avoiding it, so that I can recommend to people how to structure their apps.

Is this ComponentExample the root of your tree (i.e. is it effectively your <App/>)?

Got to go now. Can think about this more tomorrow.

shirakaba commented 5 years ago

Actually, could you also show me how you’re starting the app up?

Lelelo1 commented 5 years ago

Here is app.ts:

declare var module: any;
if(module.hot){
    // self accept.
    module.hot.accept(
        function(error: any) {
            console.error(`Error in accepting self update for app.ts.`, error);
        }
    );
}
(global as any).__DEV__ = false;
import * as React from "react";
import * as ReactNativeScript from "react-nativescript";
import HotApp, { rootRef } from "./AppContainer";
ReactNativeScript.start(React.createElement(HotApp, {}, null), rootRef);

And AppContainer.tsx containing ComponentExample:

export const rootRef: React.RefObject<any> = React.createRef<any>();
class AppContainer extends React.Component {
    pageRef = React.createRef<Page>();
    componentDidMount() {
        /* Frame and Page ios is initialized */
       const frame = rootRef.current as Frame;
       console.log("rootRef-frame ios: " + frame.ios.controller);
       console.log("page ios: " + this.pageRef.current.ios);

        rootRef.current.navigate({
            create:() => {
                return this.pageRef.current;
            }
        });
    }
    render() {
        return (
            <$Frame ref={rootRef}>
                <$Page ref={this.pageRef} 
                >
                    <ComponentExample />
                </$Page>
            </$Frame>
        )
    }
}
export default hot(() => <AppContainer />);
shirakaba commented 5 years ago

Native components are populated on onLoaded, which is after onComponentDidMount and is per-component

Both Frame and Page ios properties can be accessed however.

Ahh, I read too quickly. I was misinterpreting the problem. I see exactly what the issue is now!

Your code looks absolutely correct, except for one piece of understanding (which is fair enough, because I haven't documented it): the NativeScript wrapper Views, e.g. Page, do not populate their native elements, e.g. UIViewController, upon componentDidMount. They populate them at a later life-cycle event, onLoaded.

I'd of course prefer the native views to be ready as soon as componentDidMount, but this is a behaviour inherited from NativeScript Core, and so probably affects every other flavour of NativeScript, too. Next time I have a detailed chat with a NativeScript core team member, I'll ask about this aspect, but I don't think realistically that it can be made any more ergonomic.

So here's how to use the onLoaded event in your case:

import { isIOS } from "tns-core-modules/platform/platform";
import { EventData } from "tns-core-modules/data/observable";

export const rootRef: React.RefObject<any> = React.createRef<any>();
class AppContainer extends React.Component {
    pageRef = React.createRef<Page>();
    componentDidMount() {
        /* The refs to your frame and page should be populated at this point.
         * However, they refer only to the NativeScript wrapping view.
         *
         * Each NativeScript wrapping view populates its native view a little bit later:
         * on its onLoaded event. */
        const frame = rootRef.current as Frame;

        rootRef.current.navigate({
            create:() => {
                return this.pageRef.current;
            }
        });
    }

    private readonly onFrameLoaded = (ab: Frame) => {
        if(!isIOS){
            return;
        }

        // https://github.com/NativeScript/NativeScript/blob/master/tns-core-modules/ui/frame/frame.ios.ts#L38
        const uiNavController: UINavigationController = ab.ios.controller as UINavigationController;
    };

    private readonly onPageLoaded = (ab: Page) => {
        if(!isIOS){
            return;
        }

        // https://github.com/NativeScript/NativeScript/blob/master/tns-core-modules/ui/page/page.ios.ts#L304-L306
        const uiNavController: UIViewController = ab.ios as UIViewController;
    };

    render() {
        return (
            <$Frame
                ref={rootRef}
                onLoaded={(args: EventData) => {
                    this.onFrameLoaded(args.object as Frame);
                }}
            >
                <$Page
                    ref={this.pageRef} 
                    onLoaded={(args: EventData) => {
                        this.onPageLoaded(args.object as Page);
                    }}
                >
                    <ComponentExample />
                </$Page>
            </$Frame>
        )
    }
}
export default hot(() => <AppContainer />);

Caution: I don't know the life-cycle order of frame.navigate() with respect to the native views being initialised. If you want to do navigation via native property access, or fiddle with the native navigation components during navigation, this is unexplored territory!

About HMR

I see that you've followed my instructions for setting up an app with HMR. Well done, for a start! Just as a heads-up: I've learned that the React core team is working on totally new HMR tools that mean that we'll be able to cut out all this messy HMR boilerplate (e.g. export default hot()) soon. But they haven't given an estimated release date yet. I'll document it as soon as it's released.

Further examples of accessing native properties in RNS

Lelelo1 commented 5 years ago

Great! Using onLoaded worked.

It might be a separate issue though but how come ref={(r) => { // crash }} is not working?

shirakaba commented 5 years ago

Great to hear! Do keep me updated on how you get on with RNS.

The two types of ref

So the types of ref that I've found to work are ref objects (the ones produced by React.createRef(), which was an API introduced in React 16.3 which React DOM recommends for use in new projects).

ref={(r) => { // crash }}, is the older style of obtaining a ref (a callback ref).

RNS ref-forwards every component

Although I don't think age of the API is the problem here. You'll find that every component in React NativeScript is a ref-forwarded component (that's what this maelstrom of generics at the bottom of every component file is for).

If I recall, I did this because every component needs its own ref to manage its own lifecycle (e.g. attaching/detaching event listeners), but the developer also needs to be able to pass their own ref in. Also I thought that I'd make a bit more use of exposing the refs of child components. Now that I look back at it, I'm not totally sure it was necessary after all, but it may have solved some problem. If it's concluded that it wasn't necessary at all, that code could all be cut out and it probably wouldn't even be a breaking change.

Suspicion

I would guess that callback refs don't work because all the components are ref-forwarded, or more specifically because they're strictly expecting to call ref.current internally when attaching/detaching event listeners.

That'll probably be it. Either old-style refs could be handled transparently, or we'll need a big warning somewhere in the documentation (which doesn't exist yet) to tell devs to use only the newer API.

shirakaba commented 5 years ago

@Lelelo1 I received an email notification from you in this issue regarding onLayout(), but there's no comment here anymore – did you solve your query on your own, or do you still need it answering?

Lelelo1 commented 5 years ago

@Lelelo1 I received an email notification from you in this issue regarding onLayout(), but there's no comment here anymore – did you solve your query on your own, or do you still need it answering?

@shirakaba Did not read this until now. Unfortunately I don't recall what the problem was. So I must have found some way around it I guess - or found out that is not really specific to react-nativescript.

Lelelo1 commented 2 years ago

So what is the current way of accessing the native instance behind the components. I can't find onLoaded anymore?

My package json

  "dependencies": {
    "@nativescript/core": "~8.2.0",
    "@react-navigation/core": "^5.15.3",
    "react": "~16.13.1",
    "react-nativescript": "^3.0.0-beta.1",
    "react-nativescript-navigation": "3.0.0-beta.2"
  }

Nevermind it just appears that webView and image don't have onLoaded? searchBar has it like the docs indicate


I notice they are colored different:

components

shirakaba commented 2 years ago
  1. Try writing webView instead of webview.
  2. image is correct, but you might be getting a name-clash with image from HTML (due to @types/react annoyingly including the types for all HTML elements), so try to manually run node_modules/.bin/patch-package, which runs my patch to clean up the @types/react typings. Also, make sure that your @types/react version is exactly 16.9.34.
Lelelo1 commented 2 years ago

I did what you said and I think it gave access to webView. However image is still of html:

Screenshot 2022-06-15 at 13 20 07

shirakaba commented 2 years ago
  1. Look inside node_modules/@types/react. Make sure that the JSX.IntrinsicElements section looks like this (empty):
declare global {
    namespace JSX {
        // tslint:disable-next-line:no-empty-interface
        interface Element extends React.ReactElement<any, any> { }
        interface ElementClass extends React.Component<any> {
            render(): React.ReactNode;
        }
        interface ElementAttributesProperty { props: {}; }
        interface ElementChildrenAttribute { children: {}; }

        // We can't recurse forever because `type` can't be self-referential;
        // let's assume it's reasonable to do a single React.lazy() around a single React.memo() / vice-versa
        type LibraryManagedAttributes<C, P> = C extends React.MemoExoticComponent<infer T> | React.LazyExoticComponent<infer T>
            ? T extends React.MemoExoticComponent<infer U> | React.LazyExoticComponent<infer U>
                ? ReactManagedAttributes<U, P>
                : ReactManagedAttributes<T, P>
            : ReactManagedAttributes<C, P>;

        // tslint:disable-next-line:no-empty-interface
        interface IntrinsicAttributes extends React.Attributes { }
        // tslint:disable-next-line:no-empty-interface
        interface IntrinsicClassAttributes<T> extends React.ClassAttributes<T> { }

        // tslint:disable-next-line:no-empty-interface
        interface IntrinsicElements {
        }
    }
}

Also, restart the TypeScript Language Server. You can do this from the VS Code command palette (which is opened using cmd+shift+P) by typing "restart" and selecting "TypeScript: Restart TS server".

image