React native eslint-disable-next-line react/no-unstable-nested-components - reactjs

I am using a custom back button in the headerLeft. I have enabled ESLINT, and it is showing the below warning.
Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component “App” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.eslintreact/no-unstable-nested-components
App.tsx
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* #format
*/
import React from 'react';
import {NavigationContainer} from '#react-navigation/native';
import {createDrawerNavigator} from '#react-navigation/drawer';
import {StatusBar, useColorScheme} from 'react-native';
import {Colors} from 'react-native/Libraries/NewAppScreen';
import {useNavigation} from '#react-navigation/native';
import {DrawerContent} from './src/navigation/drawer-content';
import {Button} from 'react-native-paper';
import Feed from './src/screens/feed';
import Article from './src/screens/article';
const Drawer = createDrawerNavigator();
const CustomDrawer = (props: any) => {
return <DrawerContent {...props} />;
};
const BackButton = () => {
const navigation = useNavigation();
return (
<Button
icon="arrow-left-thick"
onPress={() => navigation.goBack()}
children={undefined}
/>
);
};
function App(): JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};
return (
<NavigationContainer>
{/* <SafeAreaView style={backgroundStyle}> */}
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
<Drawer.Navigator drawerContent={CustomDrawer} initialRouteName="Feed">
<Drawer.Screen name="Feed" component={Feed} />
<Drawer.Screen
name="Article"
component={Article}
options={{
title: 'Timeline',
headerLeft: () => <BackButton />,
}}
/>
</Drawer.Navigator>
</NavigationContainer>
);
}
export default App;

It could be because of the following eslint rule react/no-unstable-nested-components.
I guess according to react-navigation's guide you have defined the headerLeft prop but the above eslint rule contradicts that.
Contradiction occurred here:
<Drawer.Screen
name="Article"
component={Article}
options={{
title: 'Timeline',
headerLeft: () => <BackButton />, // <--- this line
}}
/>
Possible fix(es):
Disable plugin for the file: // eslint-disable react/no-unstable-nested-components (or for just one line)
Move component definition out of the component.
Edit the .eslintrc to allow component as props in this rule's settings
...
"react/no-unstable-nested-components": [
"off" | "warn" | "error",
{ "allowAsProps": true | false }
]
...

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.

onPress function for DrawerNavigator

I have created a DrawerNavigator in my react native app which looks like this.
I just dont like the default header that react- native gives. So I wanna access it through an icon. I guess also using the onPress condition
import { createStackNavigator } from '#react-navigation/stack';
import { createDrawerNavigator } from '#react-navigation/drawer';
// importing of all screens
const Drawer = createDrawerNavigator();
const DrawerContent = () => {
return (
<Drawer.Navigator>
<Drawer.Screen
name="Home"
component={CategoryStack}
/>
<Drawer.Screen name="Aboutus" component={Aboutus} />
<Drawer.Screen name="Interest Recieved" component={InterestRecieved} />
</Drawer.Navigator>
);
};
const Stack = createStackNavigator();
const MainStack = () => {
return (
<Stack.Navigator>
<Stack.Screen
name="Loading"
component={Loading}
options={{ headerShown: false }}
/>
</Stack.Navigator>
);
};
export default MainStack;
How do I open it using an onPress of an icon?
Thanks!
React navigation useNavigation hook expose drawer actions - toggleDrawer,openDrawer and closeDrawer event handlers which you can use to open or close drawer.
import React from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import { useNavigation } from "#react-navigation/native";
const ToggleDrawer = () => {
const { toggleDrawer,closeDrawer,openDrawer } = useNavigation();
return (
<Pressable onPress={toggleDrawer}>{/** Add your Icon Here */}</Pressable>
);
};
You can check in-depth drawer example
In the header options you can customize the header and add for example an icon on the top left side like this.
useLayoutEffect(() => {
navigation.setOptions({
title: 'ScreenName',
headerLeft: () => (
<View style={{ marginLeft: 15 }}>
<TouchableOpacity onPress={() => navigation.openDrawer()} >
{*INSERT ICON HERE*}
</TouchableOpacity>
</View>
),
})
})

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

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>
)
}

How to Navigate from DRAWER NAVIGATION to BOTTOM TAB BAR NAVIGATION in react native?

this url: https://streamable.com/no6anz, is a video showcasing my app's navigation, I want the ability to navigate from a Drawer screen to a SPECIFIC Tab Screen in my Tab Navigation.
For example: If i pressed let's say the 'Messages' drawer item, i would like to navigate to the My Account tab screen in the tab navigator.
So far my implementation has not been the best:
import React, {Fragment, Component} from 'react';
import {View, StyleSheet} from 'react-native';
import {createBottomTabNavigator} from '#react-navigation/bottom-tabs';
import {createDrawerNavigator} from '#react-navigation/drawer';
import {createStackNavigator} from '#react-navigation/stack';
import Home from '../Views/HomeView';
import Cart from '../Views/CartView';
import MyAccount from '../Views/MyAccountView';
import Sidebar from '../Views/Sidebar';
import HomeIconColor from './CustomSVGIcons/HomeIconColor';
import CartIconColor from './CustomSVGIcons/CartIconColor';
import PersonIconColor from './CustomSVGIcons/PersonIconColor';
import MenuIconColor from './CustomSVGIcons/MenuIconColor';
import Contact from '../Views/screens/Contact';
import Messages from '../Views/screens/Messages';
import {
NavigationContainer,
DrawerActions,
useNavigation,
} from '#react-navigation/native';
import {BottomTabBar} from '#react-navigation/bottom-tabs';
import Login from '../Views/screens/Login';
import {create} from 'react-test-renderer';
import {createSwitchNavigator} from '#react-navigation/compat';
const Tab = createBottomTabNavigator();
const Drawer = createDrawerNavigator();
const Stack = createStackNavigator();
const Switch = createSwitchNavigator();
class Navigator extends Component {
constructor() {
super();
this.state = {};
}
render() {
return (
<NavigationContainer>
<MyDrawerNavigation />
</NavigationContainer>
);
}
}
const MyTabs = (props) => {
const navigation = useNavigation();
const Tab = createBottomTabNavigator();
return (
<Tab.Navigator
screenOptions={({route}) => ({
tabBarButton: ['Contact', 'Route2ToExclude'].includes(route.name)
? () => {
return null;
}
: undefined,
})}>
<Tab.Screen
name="Home"
component={Home}
/>
<Tab.Screen
name="MyAccount"
component={MyAccount}
/>
<Tab.Screen
name="Cart"
component={Cart}
/>
<Tab.Screen
name="Sidebar"
component={Sidebar}
/>
<Tab.Screen name="Contact" component={Contact} />
</Tab.Navigator>
);
};
const MyDrawerNavigation = () => {
return (
<Drawer.Navigator drawerPosition={'right'}>
<Drawer.Screen
name="Home"
component={MyTabs}
initialParams={{screen: 'Home'}}
/>
<Drawer.Screen
name="My Account"
component={MyTabs}
initialParams={{screen: 'MyAccount'}}
/>
</Drawer.Navigator>
);
};
const styles = StyleSheet.create({
tabNav: {
backgroundColor: '#000000',
},
});
export default Navigator;
You can pass the screen as the initial param and it would redirect to the correct tab.
<Drawer.Screen name="MyTabs" component={'Tab Component Name'} initialParams={{screen:'MyAccount'}}/>
SOLVED!:
After extensively further researching; I have created my own custom Drawer, with the onPress handler.
The onPress handler initiates the action for navigation to the specified screens.
NOTE: you will have to pass in the navigation ref to the top-level navigation container. You will also have to pass this same reference into your custom drawer module (or any module in that concern)
here's my custom drawer component:
import React, {Component} from 'react';
import {Text, View, Button, StyleSheet} from 'react-native';
import {useNavigation, CommonActions} from '#react-navigation/native';
import * as rootNavigator from '../components/rootNavigator';
const CustomDrawer = () => {
return (
<View style={styles.container}>
<Button
title="My Account"
onPress={() => rootNavigator.navigate('MyAccount')}
/>
<Button
title="Contact"
onPress={() => rootNavigator.navigate('Contact')}
/>
<Button title="Cart" onPress={() => rootNavigator.navigate('Cart')} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 30,
},
});
export default CustomDrawer;
here's the code where I return the DrawerNavBar:
const MyDrawerNavigation = () => {
return (
<NavigationContainer ref={navigationRef}>
<Drawer.Navigator
drawerPosition={'right'}
drawerContent={(props) => <CustomDrawer {...props} />}>
<Drawer.Screen name="Tabs" component={MyTabs} />
<Drawer.Screen name="Contact" component={Contact} />
</Drawer.Navigator>
</NavigationContainer>
);
};

React Native: Navigating between nested navigators from different components

I am building an app with one main tab navigator, and then several nested stack navigators within it. All of my navigators are written within the App.js file. The main tab navigator is working fine, however when I attempt to navigate into one of the nested stack navigators I get a TypeError: undefined is not an object (evaluating _this.props.navigation) error.
Here is an example of my code below:
App.js File
import React from 'react;
import {createMaterialBottomTabNavigator} from '#react-navigation/material-bottom-tabs';
import {NavigationContainer} from '#react-navigation/native';
import {createStackNavigator} from '#react-navigation/stack';
import Home from './src/screens/Home';
import Settings from './src/screens/Settings';
import Privacy from './src/screens/Privacy';
const PrivacyStack = createStackNavigator();
const SettingsStack = createStackNavigator();
const AuthStack = createStackNavigator();
const MainStack = createStackNavigator();
const Tabs = createMaterialBottomTabNavigator();
const TabNavigator = () => {
return (
<Tabs.Navigator
initialRouteName="Home"
<Tabs.Screen
name="Home"
component={HomeStack}
/>
Tabs.Screen
name="Settings"
component={SettingsStack}
children={this.SettingsStack}
</Tabs.Navigator>
)
}
const AuthStack = () => (
<AuthStack.Navigator>
<AuthStack.Screen
name="Auth"
component={Auth}
/>
</AuthStack.Navigator>
);
const SettingsStackScreen = () => (
<SettingsStack.Navigator>
<SettingsStack.Screen
name="Settings"
component={Settings}
/>
<SettingsStack.Screen
name="Privacy"
component={PrivacyStack}
/>
</SettingsStack.Navigator>
);
const PrivacyStack = () => (
<PrivacyStack.Navigator>
<PrivacyStack.Screen
name="Privacy"
component={Privacy}
/>
<PrivacyStack.Screen
name="Notifications"
component={Notifications}
/>
</PrivacyStack.Navigator>
);
const App = () => {
return (
<NavigationContainer ref={navigationRef}>
<MainStack.Navigator>
<MainStack.Screen name="Tabs" component={TabNavigator} />
<MainStack.Screen
name="Auth"
component={AuthStack}
options={{gestureEnabled: false}}
/>
</MainStack.Navigator>
</NavigationContainer>
)
}
Settings.js File
import React from 'react';
import {TouchableOpacity} from 'react-native;
const Settings = (navigation, props) => {
return (
<TouchableOpacity onPress={() => this.props.navigation.navigate('PrivacyStack', {screen: 'Privacy'}
)
}
</TouchableOpacity>
export default Settings
Privacy.js File
import React from 'react';
import {TouchableOpacity} from 'react-native;
const Privacy = (navigation, props) => {
return (
<TouchableOpacity onPress={() => this.props.navigation.navigate('Notifications'
)
}
</TouchableOpacity>
export default Privacy
After the user has passed auth, they are taken to the MainStack which has a bottom tab navigator with a Home screen option, and a Settings screen option. This part is all currently working fine. However once I click the <TouchableOpacity> on the Settings screen I get the error.
I suspect it must be an issue with the way I am using this.props.navigation however I have not managed to figure out the problem through my own troubleshooting. Any help would be hugely appreciated.
You are using functional component so you can use the navigation like below
const Settings = (props) => {
return (
<TouchableOpacity onPress={() => props.navigation.navigate('PrivacyStack',
'navigation' is passed as a prop and will reside inside props argument.

Resources