How do I render screens according to state variables? - javascript

UPDATE
Someone helpfully suggested chaining ternary statements in the answers below, but I'm afraid this doesn't work. I've copied my version of their solution below. The error is Error: A navigator cannot contain multiple 'Screen' components with the same name (found duplicate screen named 'Welcome')
// This is the root stack navigator.
// It is currently the main skeleton of the navigation logic
const RootStack = createStackNavigator();
const RootStackScreen = () => {
React.useEffect(() => {
SplashScreen.hide();
}, []);
const [hasCompletedIntro, setHasCompletedIntro] = React.useState(false);
const [hasSelectedLanguage, setHasSelectedLanguage] = React.useState(true);
return (
<RootStack.Navigator>
{hasSelectedLanguage ? (
<>
<RootStack.Screen name="Welcome" component={WelcomeScreen} />
<RootStack.Screen
name="HowToUseThisApp"
component={HowToUseThisAppScreen}
/>
<RootStack.Screen name="Home" component={AppTabsScreen} />
</>
) : (
<RootStack.Screen
name="Choose Your Language"
component={ChooseYourLanguageScreen}
/>
)}
{!hasCompletedIntro && hasSelectedLanguage ? (
<>
<RootStack.Screen name="Welcome" component={WelcomeScreen} />
<RootStack.Screen
name="HowToUseThisApp"
component={HowToUseThisAppScreen}
/>
<RootStack.Screen name="Home" component={AppTabsScreen} />
</>
) : (
<RootStack.Screen name="Home" component={AppTabsScreen} />
)}
</RootStack.Navigator>
);
};
Our app uses React Native 0.63.2 and React Navigation v5. We are using functional components with hooks only, no classes.
I need to find a way to render the following screens according to the following pieces of state, so:
if hasSelectedLanguage AND hasCompletedIntro are both true, they should go to the HomeScreen.
hasSelectedLanguage is true but hasCompletedIntro is false, they should go to the WelcomeScreen.
hasSelectedLanguage is false, they should go to the ChooseYourLanguageScreen.
As you can see in the code snippet, I have already found a way to render screens according to the boolean state of hasCompletedIntro, but React Navigation 5 throws errors when I try to chain ternary statements. I'm stuck.
I would love to know how to render screens to account for the bullet points above while also retaining the navigation associated with the hasCompletedIntro ternary which is already in the code.
import 'react-native-gesture-handler';
import React from 'react';
import SplashScreen from 'react-native-splash-screen';
import AsyncStorage from '#react-native-community/async-storage';
import { NavigationContainer } from '#react-navigation/native';
import { createBottomTabNavigator } from '#react-navigation/bottom-tabs';
import { createStackNavigator } from '#react-navigation/stack';
import HomeScreen from '../screens/HomeScreen';
import WelcomeScreen from '../screens/WelcomeScreen';
import AppMenuScreen from '../screens/AppMenuScreen';
import HowToUseThisAppScreen from '../screens/HowToUseThisAppScreen';
import ChooseYourLanguageScreen from '../screens/ChooseYourLanguageScreen';
// This is the tab navigator for the bottom tabs containing the Home and More stack navigators
const AppTabs = createBottomTabNavigator();
const AppTabsScreen = () => {
return (
<AppTabs.Navigator
AppTabsBarOptions={{
labelStyle: {
fontSize: 15,
fontWeight: '600',
marginBottom: 8,
},
}}>
<AppTabs.Screen name="Home" component={HomeScreen} />
<AppTabs.Screen name="App Menu" component={AppMenuScreen} />
</AppTabs.Navigator>
);
};
// This is the root stack navigator.
// It is currently the main skeleton of the navigation logic
const RootStack = createStackNavigator();
const RootStackScreen = () => {
React.useEffect(() => {
SplashScreen.hide();
}, []);
const [hasCompletedIntro, setHasCompletedIntro] = React.useState(false);
const [hasSelectedLanguage, setHasSelectedLanguage] = React.useState(false);
return (
<RootStack.Navigator>
{hasCompletedIntro ? (
<RootStack.Screen name="Home" component={AppTabsScreen} />
) : (
<>
<RootStack.Screen
name="Choose Your Language"
component={ChooseYourLanguageScreen}
/>
<RootStack.Screen name="Welcome" component={WelcomeScreen} />
<RootStack.Screen
name="HowToUseThisApp"
component={HowToUseThisAppScreen}
/>
<RootStack.Screen name="Home" component={AppTabsScreen} />
</>
)}
</RootStack.Navigator>
);
};
export default () => {
return (
<NavigationContainer>
<RootStackScreen />
</NavigationContainer>
);
};

There are two potential problems with your attempt to chain ternary operators:
The conditions are not mutually exclusive so the same screen is rendered multiple times.
The same screen is rendered in the "else" branch of multiple ternary operators.
The solution is to remove this duplication.
The simplest approach is to make sure all conditions are mutually exclusive and that each "else" branch rendereds a unique screen. Right now your conditions are hasSelectedLanguage and !hasCompletedIntro && hasSelectedLanguage which can both be true at the same time. Insead, you could change them to hasCompletedIntro && hasSelectedLanguage and !hasCompletedIntro && hasSelectedLanguage which cannot be both true at the same time (and matches your verbal description anyway).
One solution is to nest the ternary operators so that only one branch executes:
{ <condition1> ? <screens> : (<condition2> ? <screens> : <screens>)}
But with when you fill this out with the actual JSX, the nesting and indentation will be horrendous. Instead, I suggest using JavaScript code before the return:
render() {
let screens = <default screens>;
if (<condition1>) {
screens = <screens>;
} else if (<condition2>) {
screens = <screens>;
}
return (
<RootStack.Navigator>
{screens}
</RootStack.Navigator>
;
}
The if logic can be whatever you want to get the job done. This cleanly separates that logic from the actual rendering.

Related

Can't use Swipeable with Bottom tabs, React Navigator

I have a FlatList with some swipeable components inside of it, using Swibepeable from react-native-gesture-handler. My current navigation structure goes as follows:
I have a routes folder with a bottom tabs navigator file and a stack navigator file. The stack navigator is nested inside of the tabs navigator, which looks like this:
TabsNavigator:
import { createBottomTabNavigator } from "#react-navigation/bottom-tabs";
import HomeStack from "./HomeStack";
export default function TabsNavigator() {
const Tab = createBottomTabNavigator();
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen
name="List"
component={HomeStack}
options={{
tabBarIcon: ({ focused }) => (
<FontAwesomeIcon
icon={faList}
style={{ color: focused ? "#104543" : "#CCC" }}
/>
),
}}
/>
</Tab.Navigator>
);
}
HomeStack:
import { createStackNavigator } from "#react-navigation/stack";
import { Home } from "../screens/Home";
export default function StackNavigator() {
const Stack = createStackNavigator();
return (
<Stack.Navigator>
<Stack.Screen
options={{ headerShown: false }}
name="Home"
component={Home}
/>
</Stack.Navigator>
);
}
However, since the app changed the StackNavigator (HomeStack) has become irrelevant. Thus I've tried to insert the {Home} screen into the Tab.Screen's component, thinking this would do the trick. It did do the trick, except that I can't swipe any of the rows in the FlatList anymore. I'm still able to open the Accordion ListItem inside of the FlatList however. For extra clarity I'll add the Home screen as well.
Home:
export const Home = ({ navigation }) => {
const dispatch = useDispatch();
useEffect(() => {
const fetchUserData = async () => {
const data = await axios.get(
`http://192.168.****`,
{
auth: {
username: "****",
password: "***",
},
}
);
dispatch(setUsersData(data.data));
};
return navigation.addListener("focus", async () => {
await fetchUserData();
});
}, [navigation]);
return (
<View style={styles.container}>
<SearchBarHeader/>
<FlatListComponent />
</View>
);
};
I haven't seen any similar issue online yet. Does anyone have an idea what could cause this?

Context not working properly in React-Native

Here is my Context File:
import React from 'react';
import { createContext, useState } from 'react';
export const TabContext = createContext({
opened: false,
toggleOpened: () => {},
});
export const TabContextProvider = ({ children }) => {
const [opened, setOpened] = useState(false);
const toggleOpened = () => {
setOpened(!opened);
};
return (
<TabContext.Provider value={{ opened, toggleOpened }}>
{children}
</TabContext.Provider>
);
};
My Simplified App.js File: (Necessary files are imported)
const Tab = createBottomTabNavigator();
export default function App() {
const buttonCtx = useContext(TabContext);
return (
<>
<TabContextProvider>
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen
name='Action'
component={ActionScreen}
options={{
tabBarButton: () => (
<ActionButton
opened={buttonCtx.opened}
toggleOpened={buttonCtx.toggleOpened}
/>
),
}}
/>
</Tab.Navigator>
</NavigationContainer>
</TabContextProvider>
</>
);
}
And the Simplified ActionButton Component:
export default function ActionButton({ opened, toggleOpened }) {
return (
<View style={styles.container}>
<View style={styles.box}>
<TouchableWithoutFeedback
onPress={toggleOpened}
style={styles.actionButton}
>
/* With an Animated View inside */
</TouchableWithoutFeedback>
</View>
</View>
);
}
Basically, **toggleOpened **should switch the value of the variable **opened **between true and false. So the **AnimatedView **can work properly which solely depends on the value of opened.
Opened is readable in all of the components, no problem with that. But **toggleOpened **is not working at all. Any idea?
In order to use contexts properly, you need to have at least two components working together,one that renders the Provider and one descendant who then uses that context.
You are trying to provide and use the context at the same time,try to move the consumer component one position down to the hierarchy.
For example in your App.js you can create a consumer component to wrap your ActionButton,then pass the context to it as you did :
export default function App() {
return (
<>
<TabContextProvider>
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen
name="Action"
component={ActionScreen}
options={{
tabBarButton: () => <ActionButtonWrapper />
}}
/>
</Tab.Navigator>
</NavigationContainer>
</TabContextProvider>
</>
);
}
const ActionButtonWrapper = () => {
const { opened, toggleOpened } = useContext(TabContext);
return (
<>
<ActionButton
opened={opened}
toggleOpened={toggleOpened}
/>
</>
);
};
However,i would just use the context directly within ActionButton,after all,passing props down to children is what we want to avoid by using context,right?
I have created for you a snippet to see how we can properly use context

Found screens with the same name nested inside one another

I want to achieve a native stack navigator for each tab. However I want different Stack navigator files for each tab. So I created different files but i'm getting a warning in the console:
Found screens with the same name nested inside one another. Check:
Home, Home > Home
This can cause confusing behavior during navigation. Consider using
unique names for each screen instead. at
node_modules/#react-navigation/core/src/BaseNavigationContainer.tsx:364:14 in React.useEffect$argument_0
Also the the Home screen is nested inside Home screen.I tried changing the name of the Home from Home to Homescreen component still the issue persists.
Here are the functions in my files:
Home.js
function Home({ navigation }) {
return (
<View style={styles.screen}>
<Text>Home screen!</Text>
<Button title="Details" onPress={() => navigation.navigate("Details")} />
</View>
);
}
Details.js
const Details = (props) => {
return (
<View>
<Text>Details will appear here!</Text>
</View>
);
};
HomeStackScreen.js
import { createNativeStackNavigator } from "#react-navigation/native-stack";
import HomeScreen from "../screens/Home";
import DetailsScreen from "../components/Details";
const HomeStack = createNativeStackNavigator();
function HomeStackScreen() {
return (
<HomeStack.Navigator>
<HomeStack.Screen name="Home" component={HomeScreen} />
<HomeStack.Screen name="Details" component={DetailsScreen} />
</HomeStack.Navigator>
);
}
AppNavigator.js
import { createBottomTabNavigator } from "#react-navigation/bottom-tabs";
import { NavigationContainer } from "#react-navigation/native";
import HomeStackScreen from "./HomeStackScreen";
const Tab = createBottomTabNavigator();
function AppNavigator() {
return (
<NavigationContainer>
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === "Home") {
iconName = focused ? "ios-home" : "ios-home-outline";
}
{/*other routes*/}
})}
>
<Tab.Screen name="Home" component={HomeStackScreen} />
{/*other tabs*/}
</Tab.Navigator>
</NavigationContainer>
);
}

Problem passing parameters down in React native

I am having a hard time using React StackNavigator, and passing parameters down to a screen. I am just in the stages of learning how React Native works, so maybe this is not the best practice so I am open to other suggestions if there is a better way.
function SetupsStack(props) {
console.log(props.route.params.Setup,"route is") // This has what I want
return (
<Stack.Navigator
initialRouteName="IndivdualSetup"
mode="card"
headerMode="screen"
>
<Stack.Screen
name="IndivdualSetup"
component={IndivualScreen}
//component={<IndividualScreen individual={props.route.params.Setup} />} thought this was it but its not
options={{
header: ({ navigation, scene }) => (
<Header
title="IndivdualSetup"
tabs
tabTitleSizeRight={10}
tabRightIcon={"shape-star"}
scene={scene}
navigation={navigation}
/>
),
}}
/>
</Stack.Navigator>
);
}
The component:
import React from "react";
import { ScrollView, StyleSheet, Dimensions } from "react-native";
import { Block, Text, theme } from "galio-framework";
const { width } = Dimensions.get("screen");
const thumbMeasure = (width - 48 - 32) / 3;
export default class IndivualScreen extends React.Component {
render() {
// const {
// navigation,
// route,
// } = this.props;
// const { product } = route.params;
console.log(this.props,"props are")
return (
<Block flex center>
<ScrollView
style={styles.components}
showsVerticalScrollIndicator={false}
>
<Block flex>
<Text bold size={30} style={styles.title}>
Text here
</Text>
</Block>
</ScrollView>
</Block>
);
}
}
The console log in this component does not have route as a parameter just navigation, but this only has has setParams (looks like the removed getParams in v5 and newer. However if I do:
component={<IndividualScreen individual={props.route.params.Setup} />}
I get:
Blockquote Error: Got an invalid value for 'component' prop for the screen 'IndivdualSetup'. It must be a valid React Component.
My syntax looks correct everywhere so not exactly sure what my problem is, or if it isn't working cause there is a better practice I should be following.
Thanks in advance!
instead of
component={<IndividualScreen individual={props.route.params.Setup} />}
you have to do this:
component={(props)=> <IndividualScreen individual={props.route.params.Setup} />}

Resetting screen to first Parent screen, from a nested screen (React navigation & React Native)

I've followed the documentation for creating bottom tab navigation with react-navigation v5 ("#react-navigation/native": "^5.2.3")
Currently is partially used this example in my project from docs https://reactnavigation.org/docs/bottom-tab-navigator/ to fit the needs of version 5.
Example might be following
// Navigation.tsx
import { BottomTabBarProps } from '#react-navigation/bottom-tabs';
import { TabActions } from '#react-navigation/native';
import * as React from 'react';
function Navigation({ state, descriptors, navigation }: BottomTabBarProps) {
return (
<View>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
const jumpToAction = TabActions.jumpTo(options.title || 'Home');
navigation.dispatch(jumpToAction);
}
};
return (
<TouchableOpacity
key={options.title}
accessibilityLabel={options.tabBarAccessibilityLabel}
accessibilityRole="button"
active={isFocused}
activeOpacity={1}
testID={options.tabBarTestID}
onPress={onPress}
>
{route.name}
</TouchableOpacity>
);
})}
</View>
);
}
export default Navigation;
However, I have a couple of nested StackNavigators as described in AppNavigator.tsx
AppNavigator.tsx
import { createBottomTabNavigator } from '#react-navigation/bottom-tabs';
import React from 'react';
import { AppState, AppStateStatus } from 'react-native';
import Navigation from '../components/navigation/Navigation';
import AccountScreen from '../screens/account';
import SettingsScreen from '../screens/settings';
import SupportScreen from '../screens/support';
import HomeNavigator from './HomeNavigator';
import TransactionNavigator from './TransactionNavigator';
const { Navigator, Screen } = createBottomTabNavigator();
const AppNavigator = () => {
return (
<View>
<Navigator tabBar={(props) => <Navigation {...props} />}>
<Screen
component={HomeNavigator}
name="Home"
options={{ title: 'Home' }}
/>
<Screen
component={TransactionNavigator}
name="Transactions"
options={{
title: 'Transactions' }}
/>
<Screen
component={AccountScreen}
name="Account"
options={{ title: 'Account' }}
/>
<Screen
component={SupportScreen}
name="Support"
options={{ title: 'Support' }}
/>
<Screen
component={SettingsScreen}
name="Settings"
options={{
title: 'Settings' }}
/>
</Navigator>
</View>
);
};
export default AppNavigator;
And I am aiming for resetting the nested StackNavigator each time user leaves it. So example can be HOME -> TRANSACTIONS -> TRANSACTION_DETAIL (which is part of a nested navigator) -> HOME -> TRANSACTIONS
currently, I see a TRANSACTION_DETAIL after the last step of the "walk through" path. Nevertheless, I want to see TRANSACTIONS instead. I found that if I change
if (!isFocused && !event.defaultPrevented) {
const jumpToAction = TabActions.jumpTo(options.title || 'Home');
navigation.dispatch(jumpToAction);
}
to
if (!isFocused && !event.defaultPrevented) {
navigation.reset({ index, routes: [{ name: route.name }] });
}
it more or less does the thing. But it resets the navigation, so it is unmounted and on return back, all data are lost and need to refetch.
In navigation is PopToTop() function that is not available in this scope.
Also I tried to access all nested navigators through descriptors, yet I have not found how to correctly force them to popToTop.
And the idea is do it on one place so it will be handled automatically and there would not be any need to implement it on each screen.
I have tried with navigator.popToTop() but it was not working. It may be stackNavigator and TabNavigator having a different history with the routes. I have fixed the issue with the below code. "Home" is my stack navigator name and another "Home" is screen name (Both are same for me)
tabBarButton: props => (
<TouchableOpacity
{...props}
onPress={props => {
navigation.navigate('Home', {
screen: 'Home'
})
}}
/>
),

Categories

Resources