Testing: react-native (expo) take snapshot of a Screen component inside of react-navigation - reactjs

My test attempt currently looks like this:
import React from "react"
import { NavigationContainer } from "#react-navigation/native"
import { createStackNavigator } from "#react-navigation/stack"
import { render, waitFor } from "#testing-library/react-native"
import { Provider } from "react-redux"
import { createStore } from "redux"
import rootReducer from "src/store/reducers/root-reducer"
import ProfileScreen from "src/screens/profile"
const store = createStore(rootReducer)
test("renders correctly", async () => {
const Screen = () => {
// const navigationRef = React.useRef(null) // cause "Invalid hook call. Hooks can only be called inside .."
const Stack = createStackNavigator()
return (
<Provider store={store}>
<NavigationContainer
// ref={navigationRef} // cause "Invalid hook call. Hooks can only be called inside .."
>
<Stack.Navigator>
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
</NavigationContainer>
</Provider>
)
}
const tree = render(<Screen />)
await waitFor(async () => expect(tree.toJSON()).toMatchSnapshot())
})
But it keeps throwing this:
...
console.error
The above error occurred in the <ForwardRef(NavigationContainer)> component:
in Provider (created by Screen)
in Screen
Consider adding an error boundary to your tree to customize error handling behavior.
Visit <fb link> to learn more about error boundaries
...
I can't find any useful docs/examples on the react-navigation site regrading testing a screen. (I also googled a lot around)
I can try to boil down the code to something like this:
App.json
...
export default function App() {
...
return (
<Provider store={store}>
<Navigation />
</Provider>
)
}
Navigation.js
import React from "react"
import { NavigationContainer, DarkTheme } from "#react-navigation/native"
import { createStackNavigator } from "#react-navigation/stack"
import BottomTabNavigator from "./BottomTabNavigator"
export default function Navigation() {
const navigationRef = React.useRef(null)
return (
<NavigationContainer
theme={DarkTheme}
ref={navigationRef} // https://reactnavigation.org/docs/navigation-container/#ref
>
<RootNavigator />
</NavigationContainer>
)
}
const Stack = createStackNavigator()
function RootNavigator() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Root" component={BottomTabNavigator} />
...
</Stack.Navigator>
)
}
BottomTabNavigator
import React from "react"
import { Linking, TouchableWithoutFeedback, View } from "react-native"
import { createBottomTabNavigator } from "#react-navigation/bottom-tabs"
import { createStackNavigator } from "#react-navigation/stack"
import { Feather } from "#expo/vector-icons"
import Profile from "src/screens/profile"
import SignIn from "src/screens/sign-in"
const BottomTab = createBottomTabNavigator()
export default function BottomTabNavigator() {
const TabIcon = ({ name, color }) => <Feather name={name} color={color} size={20} />
return (
<BottomTab.Navigator
tabBarOptions={{
activeTintColor: Colors.defaultWhite,
style,
}}
>
<BottomTab.Screen
name="Profile"
component={ProfileNavigator}
options={{
tabBarIcon: ({ color }) => <TabIcon name="user" color={color} />,
title: I18n.t("components.tab_navigator.profile"),
}}
/>
)
}
const ProfileStack = createStackNavigator()
function ProfileNavigator() {
return (
<ProfileStack.Navigator>
<ProfileStack.Screen name="Profile" component={Profile} options={{ headerShown: false }} />
<ProfileStack.Screen name="SignIn" component={SignIn} options={{ headerShown: false }} />
</ProfileStack.Navigator>
)
}

Related

Renderer more hooks than during the previous render

I'm working on a project with bare expo and react native. I have implemented firebase to my project. The signIn and register are working fine. I have created a auth check to send the user to another page in case he is logged in. Although I am receiving the following error when attempting to send the user to another page:
This is my App.tsx:
import React, { useEffect, useState } from 'react';
import { StatusBar } from 'expo-status-bar';
import { ThemeProvider } from 'styled-components';
import {
useFonts,
Poppins_400Regular,
Poppins_500Medium,
Poppins_700Bold
} from '#expo-google-fonts/poppins';
import AppLoading from 'expo-app-loading';
import theme from './src/global/styles/theme';
import { NavigationContainer } from '#react-navigation/native';
import { AppRoutes } from './src/routes/app.routes';
import 'intl';
import 'intl/locale-data/jsonp/pt-BR';
import { SignIn } from './src/screens/SignIn';
import auth, { FirebaseAuthTypes } from '#react-native-firebase/auth';
export default function App() {
const [ fontsLoaded ] = useFonts({
Poppins_400Regular,
Poppins_500Medium,
Poppins_700Bold
});
const [user, setUser] = useState<FirebaseAuthTypes.User | null>(null);
if (!fontsLoaded) {
return <AppLoading />;
}
useEffect(() => {
const subscriber = auth().onAuthStateChanged(setUser);
return subscriber;
}, []);
return (
<ThemeProvider theme={theme}>
<StatusBar style="light"/>
<NavigationContainer>
{ user ? <AppRoutes /> : <SignIn /> }
</NavigationContainer>
</ThemeProvider>
);
}
This is the routes code:
import React from 'react';
import { createBottomTabNavigator } from '#react-navigation/bottom-tabs';
import { Dashboard } from '../screens/Dashboard';
import { Register } from '../screens/Register';
import { useTheme } from 'styled-components';
import { Platform } from 'react-native';
import { MaterialIcons } from '#expo/vector-icons';
import { Summary } from '../screens/Summary';
const { Navigator, Screen } = createBottomTabNavigator();
export function AppRoutes() {
const theme = useTheme();
return (
<Navigator
screenOptions={{
headerShown: false,
tabBarActiveTintColor: theme.colors.orange,
tabBarInactiveTintColor: theme.colors.texts,
tabBarLabelPosition: 'beside-icon',
tabBarStyle: {
height: 88,
paddingVertical: Platform.OS === 'ios' ? 20 : 0
}
}}
>
<Screen
key="Listing"
name="Listing"
component={Dashboard}
options={{
tabBarIcon: (({ size, color }) =>
<MaterialIcons
name="format-list-bulleted"
size={size}
color={color}
/>
)
}}
/>
<Screen
key="Register"
name="Register"
component={Register}
options={{
tabBarIcon: (({ size, color }) =>
<MaterialIcons
name="attach-money"
size={size}
color={color}
/>
)
}}
/>
<Screen
key="Summary"
name="Summary"
component={Summary}
options={{
tabBarIcon: (({ size, color }) =>
<MaterialIcons
name="pie-chart"
size={size}
color={color}
/>
)
}}
/>
</Navigator>
);
}
Your issue comes from this block:
if (!fontsLoaded) {
return <AppLoading />;
}
useEffect(() => {
const subscriber = auth().onAuthStateChanged(setUser);
return subscriber;
}, []);
You are conditionally adding a hook here, because if fontsLoaded evaluates to false, you return the render function earlier and the useEffect hook is never run at the first render. However, in subsequent attempts, the fonts are probably loaded, and this causes a different number of hooks being rendered: hence the error you get.
Based on react's own "rules of hooks" guide, emphasis my own:
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders.
To fix this, simply ensure all hooks are defined before any return paths. In your case, this is done by swapping the fontsLoaded check:
// Define all hooks before any return paths!
useEffect(() => {
const subscriber = auth().onAuthStateChanged(setUser);
return subscriber;
}, []);
if (!fontsLoaded) {
return <AppLoading />;
}
return (
<ThemeProvider theme={theme}>
<StatusBar style="light"/>
<NavigationContainer>
{ user ? <AppRoutes /> : <SignIn /> }
</NavigationContainer>
</ThemeProvider>
);
It is of course sometimes difficult to catch errors like this, especially in a heavy/complicated component. If you're using eslint, there is an eslint plugin called eslint-plugin-react-hook that will cause eslint to throw a warning/error when this happens.

React Navigation: TypeError: undefined is not an object (evaluating 'navigation.navigate') Issue

Facing issues while using react-navigation navigation.navigate functions.
TypeError: undefined is not an object (evaluating
'navigation.navigate')
import React from "react";
import { AccountBackground, AccountCover, Title} from "../components/account.styles";
import { MaterialIcons } from "#expo/vector-icons";
export const AccountScreen = ({ navigation }) => {
setTimeout(() => { navigation.navigate("Login");}, 3000);
return (
<AccountBackground>
<AccountCover />
<Title>Meals To Go</Title>
<MaterialIcons name="navigate-next" size={48} color="black" />
</AccountBackground>
);
};
import React, { useState, useEffect } from "react";
import { NavigationContainer } from "#react-navigation/native";
import { createStackNavigator } from "#react-navigation/stack";
import { AccountScreen } from "../../features/account/screens/account.screen";
import { LoginScreen } from "../../features/account/screens/login.screen";
import { RegisterScreen } from "../../features/account/screens/register.screen";
const Stack = createStackNavigator();
const createScreenOptions = { headerShown: false };
export const AccountNavigator = () => {
return (
<NavigationContainer>
<Stack.Navigator screenOptions={createScreenOptions}>
<Stack.Screen name="Main" component={AccountScreen} />
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
</Stack.Navigator>
</NavigationContainer>
);};

React Native Custom Navbar issue

Hey so I am having a little brain meltdown,
I cannot for the life of me figure out how I should do this.
What I am trying to do is have my custom navbar not show on the login screen but show anywhere else.
I explained in this short video: https://cdn.discordapp.com/attachments/643261623519150090/904146526778056805/Desktop_2021.10.30_-_16.10.14.04.mp4
This is my code:
[ root ./ ] App.js
import LoginScreen from './views/login'
import HomeScreen from './views/home'
import ProfileScreen from './views/profile'
import CustomNav from './views/nav/index'
const Stack = createStackNavigator();
export default function App() {
return (
<View style={styles.Test} >
<NavigationContainer theme={DarkTheme} linking={{
config: {
screens: {
Login: "/",
Home: "/home",
Profile: "/Profile/:Username"
}
},
}}>
<Stack.Navigator
screenOptions={{
headerShown: false
}}>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
<CustomNav Nav="Mobile" />
</NavigationContainer>
</View>
);
}
[ ./nav/index] index.js
import React, {useEffect, useState} from 'react';
import {} from 'react-native';
import MobileNav from './mobile'
import WebsiteNav from './website'
const CustomNav = () => {
const [viewing, setViewing] = useState('login');
useEffect(() => {
if(viewing === 'login'){
console.log("Viewing Login")
}else{
console.log("Not Login")
}
}, []);
return(
null
);
}
export default CustomNav;

How to navigate to a page in other navigator in other file in React-native?

some people and I made an app with react native to practice, and later it turned out that we used two different navigators.
I tried to combine but I failed, and tried to google but couldn't find any satisfying answer.
I just want to call SplashScreen after signing out.
so here is the code for the initial screens including Auth aka SplashScreen:
import { NavigationContainer } from '#react-navigation/native';
import { createNativeStackNavigator } from '#react-navigation/native-stack';
import Auth from './../screens/Auth'
...
import Welcome from './../screens/Welcome'
import MainApp from './MainApp' //this contains signout page
const Stack = createNativeStackNavigator();
const RootStack = () => {
return(
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerShown:false}
}}
initialRouteName="Auth"
>
<Stack.Screen name= "Auth" component={Auth} />
...
<Stack.Screen />
<Stack.Screen name="MainApp" component={MainApp}/>
</Stack.Navigator>
</NavigationContainer>
)
and here is the code with my information and signout:
import 'react-native-gesture-handler';
import React from 'react';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import {createBottomTabNavigator} from '#react-navigation/bottom-tabs';
const Stack=createStackNavigator()
const HomeScreenOptions={
headerStyle:{backgroundColor:'lightblue'},
headerTitleStyle:{color:'black', justifyContent:'center'},
headerTintColor:'white',
}
const Tabs = createBottomTabNavigator();
const YourwordsStack = createStackNavigator();
const HomeStack = createStackNavigator();
const MyInfoStack = createStackNavigator();
const YourwordsStackScreen =()=>(
<YourwordsStack.Navigator screenOptions={HomeScreenOptions}>
<YourwordsStack.Screen name="Yourwords" component={YourwordsScreen}/>
</YourwordsStack.Navigator>
)
const HomeStackScreen =()=>(
<HomeStack.Navigator
screenOptions={HomeScreenOptions}
>
<HomeStack.Screen name="Home" component={HomeScreen} />
<HomeStack.Screen name="Yourwords" component={YourwordsScreen} />
<HomeStack.Screen name="Mywords" component={MywordsScreen}/>
</HomeStack.Navigator>
)
const MyInfoStackScreen =()=>(
<MyInfoStack.Navigator screenOptions={HomeScreenOptions}>
<MyInfoStack.Screen name="MyInfo" component={MyInfoScreen}/> // this page needs to be connected with Auth
<MyInfoStack.Screen name="MyWordsHistory" component={MyWordsHistory}/>
<MyInfoStack.Screen name="MyMessages" component={MyMessages}/>
</MyInfoStack.Navigator>
)
export default function MainApp({navigation}) {
return (
<NavigationContainer independent={true}>
<Tabs.Navigator
initialRouteName="HomeTab"
screenOptions={{
tabBarHideOnKeyboard:true,
tabBarStyle:[
{
display:'flex'
},
null
]
}}
>
<Tabs.Screen
name="YourwordsTab"
component={YourwordsStackScreen}
options={{
headerShown:false,
}}
/>
<Tabs.Screen
name="HomeTab"
component={HomeStackScreen}
options={{
headerShown:false,
}}
/>
<Tabs.Screen
name="MyInfoTab"
component={MyInfoStackScreen}
options={{
headerShown:false,
}}
/>
</Tabs.Navigator>
</NavigationContainer>
)
}
so the first problem is that we used different Navigators
and the second problem is that we can't really link a page to another one through different file.
how can I solve this?

React Native call navigator inside child navigator screen

I have a main navigator in app.js
import React from 'react';
import LoginScreen from './src/screens/LoginScreen';
import HomeScreen from './src/screens/HomeScreen';
import { Image } from 'react-native';
import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import { Constans } from './src/constants/Constant';
import SignupScreen from './src/screens/SignupScreen';
import { EventScreen } from './src/screens/EventScreen';
const navigator = createStackNavigator(
{
Login: {
screen: LoginScreen,
navigationOptions: {
headerShown: false
}
},
SignUp: {
screen: SignupScreen,
navigationOptions: {
headerShown: false
}
},
Home: {
screen: HomeScreen,
navigationOptions: {
headerLeft:()=>false,
headerTitle:()=> (
<Image style={{width:35,height:35}} source={Constans.logoImage}/>
)
}
},
Event:{
screen:EventScreen,
navigationOptions: {
headerLeft:()=>false,
headerTitle:()=> (
<Image style={{width:35,height:35}} source={Constans.logoImage}/>
)
}
}
},
{
initialRouteName: 'Home'
}
);
const AppContainer = createAppContainer(navigator);
export default class App extends React.Component {
render() {
return <AppContainer />;
}
}
and when I go HomeScreen I have also menu navigator.
import React from 'react';
import Icon from 'react-native-vector-icons/FontAwesome';
import { NavigationContainer } from '#react-navigation/native';
import { createBottomTabNavigator } from '#react-navigation/bottom-tabs';
import { TabEventsScreen } from './TabEventsScreen';
import { TabForYouScreen } from './TabForYouScreen';
import { TabProfileScreen } from './TabProfileScreen';
const Tab = createBottomTabNavigator();
export default function HomeScreen() {
return (
<NavigationContainer>
<Tab.Navigator
screenOptions=
{
({ route }) =>
(
{
tabBarIcon: ({ focused, color }) =>
{
return <Icon
name={route.name === 'Explore' ? "globe" : route.name === 'Profile' ? 'user' : route.name === 'Wallet' ? 'money' :'star'}
size={focused ? 32: 24}
color={color} />;
}
}
)
}
tabBarOptions=
{
{
activeTintColor: '#1F7A8C',
inactiveTintColor: 'gray',
}
}>
<Tab.Screen name="Explore" component={TabEventsScreen} />
<Tab.Screen name="For You" component={TabForYouScreen} />
<Tab.Screen name="Wallet" component={TabProfileScreen} />
<Tab.Screen name="Profile" component={TabProfileScreen} />
</Tab.Navigator>
</NavigationContainer>
);
}
in one tab screen I am trying to navigate Event screen of main navigator. How can I do it?
import React, { useState, useEffect } from 'react';
import { Constans } from './../constants/Constant';
import { View, ImageBackground, Text, ScrollView } from 'react-native';
import { TabEventsScreenStyle } from './../styles/TabEventsScreenStyle';
import { IncomingEvents } from './../services/TabEventsService';
import { EventCard } from './../components/Eventcard'
export function TabEventsScreen({navigation}) {
const [events, setEvents] = useState([]);
const [page, setPage] = useState(1);
_getEvents = () => {
return IncomingEvents(1, page, 20);
}
useEffect(() => {
setEvents(_getEvents().Events);
});
_OnButtonPress=(key)=>{
console.log(navigation)
navigation.navigate('Event',{itemId:key});
}
return (
<ScrollView>
<ImageBackground source={Constans.homeBackGroundImage} style={TabEventsScreenStyle.backgroundImage} >
<Text style={TabEventsScreenStyle.title}>Incoming Events</Text>
<View>
{
events.map(el =>
<Text style={TabEventsScreenStyle.text} onPress={()=> this._OnButtonPress(el.key)}>
<EventCard key={el.key} data={el}></EventCard>
</Text>
)
}
</View>
</ImageBackground>
</ScrollView>
);
}
my error is
The action 'NAVIGATE' with payload
{"name":"Event","params":{"itemId":1}} was not handled by any
navigator.
Do you have a screen named 'Event'?
If you're trying to navigate to a screen in a nested navigator, see
https://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator.
I also tried
const parent_nav=navigation.dangerouslyGetParent();
console.log(navigation);
parent_nav.navigate('Event',{itemId:key});
but parent_nav is undefined
Thanks in advance
You can navigate as if all screens were at the same level.
Navigation actions are handled by current navigator and bubble up if couldn't be handled
For example, if you're calling navigation.goBack() in a nested screen, it'll only go back in the parent navigator if you're already on the first screen of the navigator. Other actions such as navigate work similarly, i.e. navigation will happen in the nested navigator and if the nested navigator couldn't handle it, then the parent navigator will try to handle it. In the above example, when calling navigate('Messages'), inside Feed screen, the nested tab navigator will handle it, but if you call navigate('Settings'), the parent stack navigator will handle it.
The source.
When you navigate, it will try to find your screen in the current navigation and if it failed, then will try with the parent until the root navigation.
Wrap your AppContainer with NavigationContainer instead of the TabNavigator, so that both the StackNavigator and TabNavigator use the same navigation prop
export default class App extends React.Component {
render() {
return(
<NavigationContainer>
<AppContainer />
<NavigationContainer/>
)}
}
And remove NavigationContainer from the TabNavigator
Here is how I nested Stack and Tab Navigator
import React from 'react';
import {Dimensions, StatusBar, StyleSheet} from 'react-native';
import {createMaterialBottomTabNavigator} from '#react-navigation/material-bottom-tabs';
import {NavigationContainer} from '#react-navigation/native';
import {createStackNavigator} from '#react-navigation/stack';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import AScreen from './AScreen';
import BScreen from './BScreen';
import CScreen from './CScreen';
import DScreen from './DScreen';
import A from './a';
import B from './B';
import C from './C';
import D from './D';
const Tab = createMaterialBottomTabNavigator();
const Stack = createStackNavigator();
const BottomTab = () => {
return (
<Tab.Navigator
activeColor="#ff6362"
backBehavior="initialRoute"
barStyle={{backgroundColor: '#000000'}}>
<Tab.Screen
name="A"
component={AScreen}
options={{
tabBarLabel: 'A',
tabBarIcon: ({color}) => (
<MaterialCommunityIcons name="book-open" color={color} size={26} />
),
}}
/>
<Tab.Screen
name="B"
component={BScreen}
options={{
tabBarLabel: 'B',
tabBarIcon: ({color}) => (
<MaterialIcons name="people" color={color} size={26} />
),
}}
/>
<Tab.Screen
name="C"
component={CScreen}
options={{
tabBarLabel: 'C',
tabBarIcon: ({color}) => (
<MaterialIcons name="people" color={color} size={26} />
),
}}
/>
<Tab.Screen
name="D"
component={DScreen}
options={{
tabBarLabel: 'D',
tabBarIcon: ({color}) => (
<MaterialIcons name="person-pin" color={color} size={26} />
),
}}
/>
</Tab.Navigator>
);
};
export default function AdminScreen(props) {
return (
<NavigationContainer>
<StatusBar hidden />
<Stack.Navigator headerMode="none">
<Stack.Screen name="BottomTab" component={BottomTab} />
<Stack.Screen name="A" component={A} />
<Stack.Screen name="B" component={B} />
<Stack.Screen name="C" component={C} />
<Stack.Screen name="D" component={D} />
</Stack.Navigator>
</NavigationContainer>
);
}

Resources