Closed Lelelo1 closed 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.
Actually, could you also show me how you’re starting the app up?
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 />);
onLoaded
, which is after onComponentDidMount
and is per-componentBoth 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!
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.
UINavigationBar
hereGreat! Using onLoaded
worked.
It might be a separate issue though but how come ref={(r) => { // crash }}
is not working?
Great to hear! Do keep me updated on how you get on with RNS.
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).
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.
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.
@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 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.
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:
webView
instead of webview
.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
.I did what you said and I think it gave access to webView
. However image
is still of html:
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".
I am trying to access the native component of
button.ios
/textfield.ios
etc. But layouts and controls are allundefined
in the componentDidMount method.Both
Frame
andPage
ios properties can be accessed however.I have also tried to:
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 theButton
as well - coming from simply assigning ref with a anonymous method(ref) => { }
;How can I for example access the
UIButton
from theButton
in react-nativescript?