Context not working properly in React-Native - javascript

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

Related

Get only undefined value from my global variable configured with Context

I have an React Native app with two pages. On the first page I have a picker from which I need the data from in the second page. I try to use Context for making sate globally available but I didn't get it to work till now because I only get undefined types at the position where I wanna insert the global state and not the value who was selected from the picker. I dont't get any errors but the field where the picker value should be represented is empty.
File from which I wanna get state from:
const FirstFile = () => {
const [selectedValueRound, setSelectedValueRound] = useState("10 rounds");
return (
<View>
<RoundContext.Provider
value={[selectedValueRound, setSelectedValueRound]}
>
<View>
<Picker
selectedValue={selectedValueRound}
onValueChange={(itemValue, itemIndex) =>
setSelectedValueRound(itemValue)
}
>
<Picker.Item label="1 round" value="0"></Picker.Item>
<Picker.Item label="2 rounds" value="1"></Picker.Item>
</Picker>
</View>
</RoundContext.Provider>
</View>
);
};
Context file:
export const RoundContext = createContext(false);
Navigation file where I wrap my context around
const Stack = createNativeStackNavigator();
const {selectedValueRound, setSelectedValueRound} = useContext(RoundContext);
const MyStack = () => {
return (
<RoundContext.Provider value={[selectedValueRound, setSelectedValueRound]}>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="FirsFile" component={FirsFile} />
<Stack.Screen name="SecondFile" component={SecondFile} />
</Stack.Navigator>
</NavigationContainer>
</RoundContext.Provider>
);
};
File where I try to insert the global value:
const SecondFile = () => {
const [selectedValueRound, setSelectedValueRound] = useContext(RoundContext);
return (
<View>
<Text>{selectedValueRound}</Text>
</View>
);
};
export default SomeFile;
You also need to define context provider and wrap your app into it.
export const RoundContextProvider = ({children}) => {
const stateTuple = useState(false);
return <RoundContext.Provider value={stateTuple}>{children}</RoundContext.Provider>;
}
<RoundContextProvider>
<YourApp/>
</RoundContextProvider>
then you can use it as you described in the question: const [selectedValueRound, setSelectedValueRound] = useContext(RoundContext);
You must declare the state and the context provider in the top parent component. The children should only consume the values from the context.
The parent component
const MyStack = () => {
const [selectedValueRound, setSelectedValueRound] = useState("10 rounds");
const contextValue = useMemo(
() => [selectedValueRound, setSelectedValueRound],
[selectedValueRound]
);
return (
<RoundContext.Provider value={contextValue}>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="FirsFile" component={FirsFile} />
<Stack.Screen name="SecondFile" component={SecondFile} />
</Stack.Navigator>
</NavigationContainer>
</RoundContext.Provider>
);
};
Note that I used useMemo to prevent passing a new array to the context when selectedValueRound did not change.
The children
const FirstFile = () => {
const [selectedValueRound, setSelectedValueRound] = useContext(RoundContext);
return (
<View>
<View>
<Picker
selectedValue={selectedValueRound}
onValueChange={itemValue => setSelectedValueRound(itemValue)}
>
<Picker.Item label="1 round" value="0"></Picker.Item>
<Picker.Item label="2 rounds" value="1"></Picker.Item>
</Picker>
</View>
</View>
);
};
const SecondFile = () => {
const [selectedValueRound] = useContext(RoundContext);
return (
<View>
<Text>{selectedValueRound}</Text>
</View>
);
};

undefined function when passing navigation AND props to a react native component (basic disconnect button)

I'm a complete newbie and I'm trying to have a stack navigator only accessible if the user is logged, which works, but I can't manage to have a correctly working disconnect button.
I'm using a simple bool so far to grant access. The function used to disconnect, passed as a prop, is not found when I'm using the disconnect button.
App/Login screen :
const Stack = createStackNavigator();
export default function App() {
const [userIsLogged, setUserLog] = useState(false);
if (!userIsLogged) {
return <LoginScreen setUserLog={setUserLog}/>;
} else {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home">
{props => <Home {...props} setUserLog={setUserLog}/>}
</Stack.Screen>
<Stack.Screen name="Rooms" component={Rooms}/>
</Stack.Navigator>
</NavigationContainer>
);
}
}
Where the disconnect button is called:
const Home = ({navigation}, props) => {
return (
<View style={styles.container}>
<Text>Home</Text>
<TouchableOpacity onPress={() => props.setUserLog(false)}>
<Text>DISCONNECT</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => navigation.navigate('Rooms')}>
<Text>ACCESS TO ROOMS</Text>
</TouchableOpacity>
<StatusBar style="auto" />
</View>
);
};
(Code has been simplified heavily to only highlight my issue)
Change
const Home = ({navigation}, props) => {
to
const Home = (props) => {
const {navigation} = props;
return /* REST OF CODE */
({navigation}, props) doesn't split the props into two groups, it defines two arguments to your function. Which means props will always be undefined since you never pass a second argument in this case.

How to pass a function to update state in Navigation Screen Component - React Native

I'm fairly new to React Native and I have created a Drawer Navigator in my App.js file.
One of my navigation components is named LifeScreen.
I am trying to pass a function to update a state (setSavedQuotes) to LifeScreen so that in LifeScreen I can update the value of the state (savedQuotes). This is straightforward in React but I can't seem to be able to do the same in React Native.
App.js (Navigator)
const App = () => {
const [savedQuotes, setSavedQuotes] = useState([])
return (
<NavigationContainer>
<Drawer.Navigator>
<Drawer.Screen
name="Home"
component={HomeScreen}
initialParams={{ sam: savedQuotes }}
/>
<Drawer.Screen
name="Life"
component={props => {
return <LifeScreen props={props} setSavedQuotes={setSavedQuotes} />
}}
/>
<Drawer.Screen name="Work" component={WorkScreen} />
<Drawer.Screen name="Saved" component={SavedScreen} />
</Drawer.Navigator>
</NavigationContainer>
);
}
LifeScreen.js
const LifeScreen = ({ route, setSavedQuotes }) => {
const [quote, setQuote] = useState('')
const lifeWisdom = [...] // array of quotes
const getQuote = () => {... } // get Random Quote
const saveQuote = () => {
// console.log('life:', route.params)
console.log("function: ", setSavedQuotes); // returns undefined
}
return (
<View style={styles.mainContainer}>
<TouchableOpacity onPress={getQuote} style={styles.quoteContainer}>
<Text style={styles.quote}>{quote}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={saveQuote} style={styles.imgContainer}>
<AntDesign name="hearto" size={40} color="black" />
</TouchableOpacity>
</View>
);
};
Whenever I console.log(setSavedQuotes), I get undefined.
I can pass props and the state value without any problem, as I did with HomeSCreen in App.js.
I tried the following: regarding props, initialParams, react-navigation and route. I can manage to pass the state with all of them, but not the function to update the state.

undefined is not an object (evaluating 'navigation.navigate') React Native

I'm have a problem in react navigation, error message "undefined is not an object (evaluating 'navigation.navigate')".
Error happens when trying to call a screen by a button from another route
Component code:
export default function Estruturas({ title, display, navigation }) {
const [estruturas, setEstruturas] = useState([]);
useEffect(() => {
async function loadEstruturas() {
const response = await api.get('/produto/api/estrutura');
setEstruturas(response.data);
}
loadEstruturas();
}, []);
return (
...
<ProductList>
{estruturas.map(estrutura => (
<Item key={estrutura.id} onPress={()=> navigation.navigate('Pedidos')}>
...
Routes code:
const Tab = createMaterialBottomTabNavigator();
function MyTabs(){
return(
<Tab.Navigator
barStyle={{ backgroundColor: '#694fad' }}
initialRouteName='Feed'
activeColor='black'
>
<Tab.Screen
name="Início"
component={Dashboard}
options={{
tabBarLabel: 'Início',
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons name="home" size={26} />
),
}}
/>
<Tab.Screen
name="Pedidos"
component={Requests}
options={{
tabBarLabel: 'Pedidos',
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons name="assignment" size={26} />
),
}}
/>
</Tab.Navigator>)}
export default function Routes() {
return (
<NavigationContainer>
<MyTabs />
</NavigationContainer>
);
}
I tried with Stack but it gave the same error
Only the direct child of your navigator stack will have access to the navigation prop. I can see that your component Estruturas is not a direct route so it will not get the navigation prop directly inside the function props. There are 2 ways to get this done and fixed.
Either use a withNavigation HOC provided by react-navigation v4 or if you are using v5 then use useNavigation hook.
OR you need to pass the navigation prop to the component wherever you are calling this component. This can be done as below:-
export default function Dashboard(props) {
return (
<View>
<Estruturas navigation={props.navigation} />
</View>
);
}

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