Navigation between screens breaks in Expo app after compilation - javascript

I've just finished building an Expo app along with TypeScript. Everything seems to be okay in the live testing mode, but, after compiling the app into a standalone app (for Android at least, haven't tried for ios), the navigation between the screens seems to be broken. I cannot even go past the first screen (when I press on the next button, the app just crashes immediately), although I know all screens are doing just fine in isolation. I am using Expo version 4.4.1, under the managed workflow.
This is my NavigationStack.tsx:
import { createStackNavigator } from 'react-navigation-stack';
import { createAppContainer } from 'react-navigation';
import EntireMapScreen from '../app/screens/EntireMapScreen';
import CardFormScreen from '../app/screens/CardFormScreen';
import ChooseOfferScreen from '../app/screens/ChooseOfferScreen';
import DecisionScreen from '../app/screens/DecisionScreen';
const screens = {
Decision: {
screen: DecisionScreen
},
ChooseOffer: {
screen: ChooseOfferScreen
},
Payment: {
screen: CardFormScreen
},
EntireMap: {
screen: EntireMapScreen
}
}
const navigationStack = createStackNavigator(screens, {
defaultNavigationOptions: {
headerShown: false
}
});
export default createAppContainer(navigationStack);
The way one file generally looks like is this (DecisionScreen.tsx):
import React from "react";
import { NavigationStackProp, NavigationStackScreenProps } from "react-navigation-stack";
import AsyncStorage from '#react-native-async-storage/async-storage';
import { NavigationInjectedProps, withNavigation } from "react-navigation";
/**
* An extremly simple non-GUI screen, which decides if the user should pay again for the app
* or if the app can simply be used.
*/
class DecisionScreen extends React.Component<NavigationInjectedProps, {
form: {
status: {
cvc: string,
expiry: string,
name: string,
number: string
},
valid: boolean,
values: {
cvc: string,
expiry: string,
name: string,
number: string,
type: string
}
},
fontsLoaded: boolean,
waitingServerResponse: boolean,
showError: boolean // if true, we know something went wrong billing the user with
// the currently inserted details
}> {
keyboardDidHideListener: any;
constructor(props: any) {
super(props);
this.state = {
form: {
status: {
cvc: "incomplete",
expiry: "incomplete",
name: "incomplete",
number: "incomplete"
},
valid: false,
values: {
cvc: "",
expiry: "",
name: "",
number: "",
type: ""
}
},
fontsLoaded: false,
waitingServerResponse: false,
showError: false
};
}
makeDecision = async (trial: number) => {
...
}
render = () => <></>;
componentDidMount = () => {
this.makeDecision(1);
}
}
export default withNavigation(DecisionScreen);
I've lost the last 6 hours or so in finding a similar situation on the internet. The best I could find was this article: https://dev.to/andreasbergqvist/react-navigation-with-typescript-29ka, which did not solve the issue. Does anybody know how I could be solving this issue?

The problem was finally due to the fact there was a promise in an async function which did not resolve until the moment of being called later on as an undefined. Therefore, the solution was to simply add an await statement next to that promise.

Related

React internalisation SPA using i18next

I need to translate my app, but i don't knomw how to use useTranslation() in the top-level files (i store there some consts which contain some text). One of this file is
import { useTranslation } from "react-i18next";
const {t} = useTranslation()
export const selectThemeOptions = [
{ value: "choose", text: "Choose theme" },
{ value: "Algebra", text: "Algebra" },
{ value: "Geometry", text: "Geomerty" },
{ value: "Programming", text: "Programming" },
{ value: "Logic", text: "Logic" },
];
so in this case i have an error:
src\Configs\ThemesOfProblems.js
Line 3:13: React Hook "useTranslation" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks
I need this array in my component, and it use in the next fragment :
<Form.Group as={Col} controlId="problemTheme">
<Form.Label>{t("userprofile.theme")}</Form.Label>
<Form.Select
name="theme"
value={values.theme}
onChange={handleChange}
isValid={touched.theme && !errors.theme}
isInvalid={!!errors.theme}
onBlur={handleBlur}
>
{selectThemeOptions.map((el, index) => {
return <option key={index} value={el.value}> {el.text} </option>
})}
</Form.Select>
</Form.Group>
And i've got a lot of such situations, i don't have any ideas how to do it
Basically it says it has to be called in a react component. It could be called in a functional component where you return your jsx or a class component that has a render method in it. If you call the function outside of one of these, then you will get this error.
You called const {t} = useTranslation(); outside of a React component, your selectThemeOptions file seems to be regular JavaScript due to the absence of JSX or a returning statement with your HTML.
Here is the correct way to do it:
/* Everything above this point is considered top-level, hence using your `useTranslation()` hook here would cause an error */
const Component = (props) => {
const { t } = useTranslation();
}
export default Component;
Here is a way to organise your translations:
• Your src folder should contain an i18n.js file with the following code:
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en from "../locales/en.json";
import ru from "../locales/ru.json";
const isReturningUser = "lang" in localStorage; // Returns true if user already used the website.
const savedLanguage = JSON.parse(localStorage.getItem("lang")); // Gets persisted language from previous visit.
// Get previously used language in case of returning user or set language to Russian by default.
const language = isReturningUser ? savedLanguage : "ru";
const resources = {
en: {
translation: en,
},
ru: {
translation: ru,
},
};
i18n.use(initReactI18next).init({
resources,
lng: language,
keyseparator: false,
interpolation: {
escapeValue: false,
},
});
export default i18n;
Your src folder should contain a locales folder with json files of the languages your application uses. Example: ru.json and en.json:
{
"choose": "Выбрать",
"chooseATheme": "Выбрать тему",
"algebra": "Алгебра",
"geometry": "Геометрия",
"programming": "Программирование",
"logic": "Логика",
}
Your component should look like this – note that the translations are in json files instead of your React component – :
import React from "react";
import { useTranslation } from "react-i18next";
const Component = (props) => {
const { t } = useTranslation();
const selectThemeOptions = [
{ value: t("choose"), text: t("chooseATheme") },
{ value: t("algebra"), text: t("algebra") },
{ value: t("geometry"), text: t("geometry") },
{ value: t("programming"), text: t("programming") },
{ value: t("logic"), text: t("logic") },
];
return( //Your UI )
}
export default Component;
This way, your translations wouldn't be hard-coded on your selectThemeOptions and will adapt to whichever translation your json locales contain.
Please tell me if this works.
Edit: If you want a concrete example of implementation of my solution here it is: https://github.com/YHADJRABIA/ecommerce/tree/main/src
Edit2: There might be a better solution of doing this, this is merely what worked for me.
Edit3: Following Nikita's comment, here's a solution to use the translation function outside of a react component —How to use react-i18next inside BASIC function (not component)?
P.S. Since you are from Belarus I assume that you want your translation to be made in Russian since Belarusian isn't as widely spoken.

Can you have complete coverage when using the pure component pattern to test Gatsby's static queries?

According to Gatsby docs, components that use static queries need to be split into variants: one being a pure component and the other that actually uses the static query, passing results to the pure component.
Given an example component:
import React from "react";
import { Helmet } from "react-helmet";
import { useStaticQuery, graphql } from "gatsby";
export const PureSEO = ({
description = ``,
lang = `en`,
meta = [],
title = ``,
image = ``,
...metaData
}): JSX.Element => {
const metaDescription = description;
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: metaData.author,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
{
name: `og:image`,
content: image,
},
{
name: `twitter:image`,
content: image,
},
{
name: `image`,
content: image,
},
].concat(meta)}
/>
);
};
const SEO: React.FC<Props> = ({
description = ``,
lang = `en`,
meta = [],
title = ``,
}): JSX.Element => {
const { site, image } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
title
description
author
rootDir
}
}
image: file(relativePath: { eq: "favicon.png" }) {
childImageSharp {
fixed(width: 400) {
...GatsbyImageSharpFixed
}
}
}
}
`
);
return (
<PureSEO
{...site.siteMetadata}
description={description}
image={image}
lang={lang}
meta={meta}
title={title}
/>
);
};
interface Props {
description?: string;
lang?: string;
meta?: HTMLMetaElement[];
title: string;
}
export default SEO;
I don't get full test coverage because SEO is not tested; but I don't think I should be testing that component, since the only logical difference is that it is using static queries.
How can I achieve better or 100% test coverage for my components, given this pattern?
Problem
I understand the intention of the Gatsby documentation avoid executing useStaticQuery, but the approach implicitly recommends leaving code uncovered.
Recommendation
To achieve code coverage, the most logical thing to do would be to either mock useStaticQuery or better yet use MockProvider.
GraphQL Testing: MockProvider - https://www.apollographql.com/docs/react/development-testing/testing/#mockedprovider
This problem is quite similar to how you would do unit testing of a backend component that communicates with some kind of external service, such as an SQL database.
You could of course mock useStaticQuery which will give you higher coverage, but it will not actually make you any surer that your code will work. What you want to test revolves around the correctness of your GraphQL query and whether the response format is what you expect, which is not something you can unit test.
So if you want to be certain that your code works you'd need to perform integration testing, e.g. by running Cypress tests against the built website.
Theoretically, you could also use contract testing to check that your query itself performs as expected, but it seems like an unnecessarily complicated solution for this usecase.

Import ES Module in react native unidentified

I want to import screen to my react navigation but when i import class React.Component its unidentified
my routes.js
import * as Screens from '../screens/index';
import {FontIcons} from '../assets/icons';
export const MainRoutes = [
{
id: 'LoginMenu',
title: 'Marketing',
icon: FontIcons.login,
screen: Screens.GridV1,
children: [
{
id: 'Login1',
title: 'Login V1',
screen: Screens.GridV1,
children: []
},
{
id: 'Login2',
title: 'Login V2',
screen: 'GridV2',
children: []
},
{
id: 'SignUp',
title: 'Sign Up',
screen: 'GridV2',
children: []
},
{
id: 'password',
title: 'Password Recovery',
screen: 'GridV2',
children: []
},
]
}
];
My screens/index.js
export * from './navigation';
export * from './dash';
When i check the Screens import with
console.log(Screens);
Everythings is well. But when i execute
console.log(Screens.GridV1);
i cant reach the GridV1 class
Please help me to solve my problem here. Thanks you
From your chrome snapshot of Screens, it shows that at that moment you console.log, Screens object only contains one element {__esModule: true}. GridV1, GridV2 and all other modules were resolved late with a delay.
Therefore you should see it works with setTimeout, ex:
setTimeout( () => console.log(Screens.GridV1), 100 );
But the real problem still hides behind. Normally import javascript module won't have such side effect, it should have works as you expected. Check if there are any special initialization mechanism of those modules.

How to navigate from a child Stack Navigator back to parent while resetting navigation stack at the same time, in React Native

I've read countless react-navigation docs, and I know there is way to do this, but it's definitely what I would call non-trivial and definitely non-intuitive.
I have a root navigation stack:
export const NavigationStack = StackNavigator({
Splash: {
screen: Splash
},
Signup: {
screen: Signup
},
Login: {
screen: SignIn
},
ForgottenPassword: {
screen: ForgottenPassword
},
Discover: {
screen: Discover
},
ProfileShow: {
screen: ProfileShow
}
}, {
headerMode: 'none'
})
The ForgottenPassword screen is a child Stack Navigator:
import { StackNavigator } from 'react-navigation'
import PasswordResetProcess from './index'
const ForgottenPassword = StackNavigator({
ResetPassword: {
screen: PasswordResetProcess
}
}, {
headerMode: 'none'
})
export default ForgottenPassword
On that index.js Container Component, there is a sub-component that I pass navigation to, like this:
switch (lastCompletedStep) {
case NEW_RESET_REQUEST:
return <InputTel navigation={navigation} />
case INPUT_TEL:
return <ResetPassword navigation={navigation} />
That ResetPassword component is the one in question. It triggers an action creator and passes this.props.navigation into the action creator:
await props.handleResetSubmit(token, props.navigation)
From inside this action creator, props.navigation is available as navigation. I can do this fine:
navigation.navigate('Discover') // see how this is from the root Navigation Stack
I cannot, however, do this:
navigation.dispatch({
type: 'Navigation/RESET',
index: 0,
actions: [{ type: 'Navigate', routeName: 'Discover' }]
})
It throws this error:
[edit] I just tried this and it also generated the same error:
navigation.dispatch(NavigationActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'Discover' })]
}))
How do I reset the stack while navigating to Discover from here?
I feel like the answer is to navigate to discover and reset the stack at the same time as some kind of child operation, but I don't know where to begin putting that together. The react-navigation documentation is horrendous for illustrating child to parent operations.
Here is my best guess at what it approximately has to look like:
navigation.dispatch({
type: 'Navigation/NAVIGATE',
routeName: 'Discover',
actions: [{ type: 'Reset', index: 0, key: null }]
})
I just solved it with this code:
navigation.dispatch(NavigationActions.reset({
index: 0,
key: null,
actions: [NavigationActions.navigate({ routeName: 'Discover' })]
}))
The secret was to add key: null, which I have seen people doing before. It is a very important element for times when you are resetting.
Here is the documentation I found that illustrates it:
https://github.com/react-community/react-navigation/issues/1127
I think this works because NavigationActions has knowledge of the root navigation stack, so it works for the same reason navigation.navigate('Discover') worked (in the context of my code in this question).
in version >2 of react navigation, NavigationActions.reset() doesnt work.
You should use StackActions.reset() instead:
import { NavigationActions, StackActions } from 'react-navigation';
const resetStackAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'Discover' })],
});
this.props.navigation.dispatch(resetStackAction);

App startup screen routing using react-native-navigation package

I'm working on a React Native app w/Meteor and have been trying out a few npm packages to determine which can best handle both native tabbed and drawer navigation. The react-native-navigation package seems to do a really great job - but it seems to be bypassing the logic I had been using to determine the user logged in state on startup.
Here's the App.js file, which is imported into both the Android and iOS index files on load:
import React from 'react';
import Meteor, { createContainer } from 'react-native-meteor';
import LoggedOut from './layouts/LoggedOut';
import LoggedIn from './layouts/LoggedIn';
import Loading from './components/Loading';
import config from './config';
Meteor.connect(config.METEOR_URL);
const MyApp = (props) => {
const { status, user, loggingIn } = props;
if (status.connected === false || loggingIn) {
return <Loading />;
} else if (user !== null) {
return <LoggedIn />;
} else {
return <LoggedOut />;
}
};
MyApp.propTypes = {
status: React.PropTypes.object,
user: React.PropTypes.object,
loggingIn: React.PropTypes.bool,
};
export default createContainer(() => {
return {
status: Meteor.status(),
user: Meteor.user(),
loggingIn: Meteor.loggingIn(),
};
}, MyApp);
And I've placed the react-native-navigation Navigation.startTabBasedApp() code in the <LoggedIn/> Component as such:
import { Navigation } from 'react-native-navigation';
import { registerScreens } from '../helpers/navigation';
registerScreens();
Navigation.startTabBasedApp({
tabs: [
{
label: 'Records',
screen: 'screen.Records',
icon: require('../images/NavigatorMenuIcon1.png'),
selectedIcon: require('../images/NavigatorMenuIcon1.png'),
title: 'Records'
},
{
label: 'Favorites',
screen: 'screen.Favorites',
icon: require('../images/NavigatorMenuIcon2.png'),
selectedIcon: require('../images/NavigatorMenuIcon2.png'),
title: 'Favorites'
},
],
drawer: {
left: {
screen: 'screen.Menu'
},
},
passProps: {},
});
The react-native-navigation package does require a bit of Xcode configuration for iOS, including adding the native files of the dependency react-native-controllers to the Xcode project; and modifying the AppDelegate.m file... Though I'm unable to figure out why the startup configuration is effectively being ignored.
UPDATE:
I found a closed issue at react-native-navigation that explains how to use layouts for hybrid (tabbed/single page) apps: you simply start a new "app" instance. For example, in my app, I want the logout function to redirect to the Sign In screen.
This can be accomplished by calling a function that starts a new single screen app instance (Navigation.startSingleScreenApp). I tested this and confirm it works for LogOut. A similar solution can be employed for the sign in / start screens.
handleSignOut(props) {
Meteor.logout(props);
Navigation.startSingleScreenApp ({
screen: {
screen: 'screen.SignIn', // unique ID registered with Navigation.registerScreen
navigatorStyle: {navBarHidden: true}, // override the navigator style for the screen, see "Styling the navigator" below (optional)
navigatorButtons: {} // override the nav buttons for the screen, see "Adding buttons to the navigator" below (optional)
},
});
The issue persists on App startup, however, as the Navigation apps are loading as they're imported into the App/index file. Essentially, the if/else logic defining app state is being ignored.

Categories

Resources