React Native provider contexts and handling multiple contexts - react-native

So, I want to consume a context TaskContext in just two components. There is a way to do this, without wrapping my whole application with the TaskContext ? Furthemore, what would be the best way to handle multiples context ?
I've try wrapping a single component with the context by creating another function that returns a new component. For example:
const HomePageContext = (props) => {
return (
<TaskProvider>
<HomePage {...props}/>
</TaskProvider>
)
}
But with this approach, the data update in AddTask screen doesn't appear in HomePage.
(Disclaimer: I just want to create a homePage that contains a list of tasks. This tasks are created in another screen, AddTasks. When the task is created, the homepage should be update to display the new list, with all other tasks and the new one. Thus, i dont know what is the best approach to do this.)
My code is here. It's only for test and practice purpose.
import React, { createContext, useReducer, useContext} from 'react'
import { View, FlatList, Text, Button } from 'react-native'
import { createNativeStackNavigator } from '#react-navigation/native-stack'
import { createDrawerNavigator } from '#react-navigation/drawer'
import { NavigationContainer } from '#react-navigation/native'
const TaskContext = createContext({})
const initialState = [{ name: `Task name ${Math.floor(Math.random() * 100)}`, id: Math.floor(Math.random() * 100) }]
//contexto provider
const TaskProvider = ({ children }) => {
const reducer = (state, action) => {
switch(action.type) {
case 'addTask':
console.log([...state, action.payload])
return [...state, action.payload]
default:
return state
}
}
const [tasks, dispatch] = useReducer(reducer, initialState)
return (
<TaskContext.Provider value={ { tasks, dispatch } }>
{ children }
</TaskContext.Provider>
)
}
const HomePage = ({ navigation }) => {
const { tasks, dispatch } = useContext(TaskContext)
return (
<View>
<Text>Home</Text>
<FlatList
data={tasks}
keyExtractor={item => `${item.id}`}
renderItem={({ item }) => <Text>{item.id} - {item.name}</Text>}
/>
<Button
title='Add Task'
onPress={() => navigation.navigate('AddTask')}
/>
</View>
)
}
const Info = () => {
return (
<View>
<Text>Info</Text>
</View>
)
}
const AddTaskPage = () => {
const { dispatch } = useContext(TaskContext)
const newTask = {id: Math.floor(Math.random() * 100), name: `Task name ${Math.floor(Math.random() * 100)}`}
return (
<View>
<Text>addTaskPage</Text>
<Button
title='AddTask'
onPress={() => dispatch({
type: 'addTask',
payload: newTask
})}
/>
</View>
)
}
// createNavigators
const Stack = createNativeStackNavigator()
const Drawer = createDrawerNavigator()
const DrawerNavigation = () =>{
return (
<Drawer.Navigator>
<Drawer.Screen name='Home' component={HomePage}/>
<Drawer.Screen name='Info' component={Info}/>
</Drawer.Navigator>
)
}
// App component
export default App = () => {
return (
<TaskProvider>
<NavigationContainer>
<Stack.Navigator initialRouteName='Menu'>
<Stack.Screen name='Menu' options={{ headerShown: false }} component={DrawerNavigation} />
<Stack.Screen name='AddTask' component={AddTaskPage} />
</Stack.Navigator>
</NavigationContainer>
</TaskProvider>
)
}

Related

Conditional navigation inside Tabs

i want to show a screen depending on a state.
So when I click on the bottom left tab, if there is a valid user, I want to be redirected to the UserProfile-screen, else redirect to the LoginScreen.
The react-native navigation confuses me, i just cant see whats wrong.
So in LoginRoutes.tsx I just try to change this behaviour by using true / false
Thanks in Advance.
What I have so far:
BottomTabBar.tsx:
export const BottomTabNavigator = () => {
const colorScheme = useColorScheme();
return (
<Tab.Navigator
screenOptions={{
tabBarShowLabel: true,
tabBarStyle: {
backgroundColor: "#292929",
borderTopWidth:0
},
tabBarInactiveTintColor: "#919191",
tabBarInactiveBackgroundColor: "#292929",
tabBarActiveTintColor: Colors[colorScheme].text,
headerShown: false,
}}
>
<Tab.Screen
name="LoginRoutes"
component={LoginRoutes}
options={{ title: "Login", headerShown: false}}
/>
<Tab.Screen
name="SettingsTabScreen"
component={SettingsTabScreen}
options={{ headerShown: false,title:"test" }}
/>
</Tab.Navigator>
);
};
LoginRoutes.tsx
import * as React from "react";
import { ActivityIndicator, View, Text } from "react-native";
import AsyncStorage from "#react-native-async-storage/async-storage";
import { createStackNavigator } from "#react-navigation/stack";
import NavigationContainer from "./UserProfileStack";
import LoginScreen from "../Screens/LoginScreen";
import UserProfile from "../components/UserProfile";
import Colors from "../constants/Colors";
import { UserProfileInfo } from "../constants/Types";
function LoginRoutes({ navigation }: { navigation: any }) {
const [loading, setLoading] = React.useState(true);
const [user, setUser] = React.useState(null);
const Stack = createStackNavigator();
React.useEffect(() => {
// check if the user is logged in or not
AsyncStorage.getItem("user")
.then((userString) => {
if (userString) {
console.log("-----------EXISTING------------");
console.log(JSON.parse(userString).id);
setUser(JSON.parse(userString));
} else {
console.log("not logged in, showing LoginPage");
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
}, []);
if (loading) {
return (
<View
style={{
height: "100%",
justifyContent: "center",
backgroundColor: Colors.dark.background,
}}
>
<ActivityIndicator
size="large"
style={{ backgroundColor: Colors.dark.background }}
/>
<Text
style={{
color: Colors.dark.text,
marginTop: 10,
alignSelf: "center",
}}
>
retrieving userdata...
</Text>
</View>
);
}
return (
<NavigationContainer>
<Stack.Navigator>
{true ? (
<Stack.Screen name="LoginScreen" component={LoginScreen} />
) : (
<Stack.Screen name="UserProfile" component={UserProfile} />
)}
</Stack.Navigator>
</NavigationContainer>
);
}
export default LoginRoutes;
The stackNavigator:
import { createStackNavigator } from "react-navigation-stack";
import { createAppContainer } from "react-navigation";
import LoginScreen from "../Screens/LoginScreen";
import UserProfile from "../components/UserProfile";
import { UserProfileInfo } from "../constants/Types";
import { StackNavigationProp } from '#react-navigation/stack';
export type UserProfileStackParams = {
LoginScreen: undefined,
UserProfile: { profileInfo: UserProfileInfo };
};
const screens = {
LoginScreen: {
screen: LoginScreen,
navigationOptions: {headerShown: false,gestureEnabled:false},
},
UserProfile: {
screen: UserProfile,
navigationOptions: {headerShown: false,gestureEnabled:false},
},
};
// home stack navigator screens
const UserProfileStack = createStackNavigator(screens);
export default createAppContainer(UserProfileStack);
UserProfile.tsx
type Props = {
navigation: StackNavigationProp<UserProfileStackParams, "UserProfile">
loggedInUser: {}
};
const DATA = [
{
// contains valid data
},
];
export const UserProfile: React.FC<Props> = ({ navigation}) => {
const [steamID, setSteamID] = React.useState({ id: null, watchLists: null });
const [profileInfo, setProfileInfo] = React.useState<UserProfileInfo>(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
// check if the user is logged in or not
AsyncStorage.getItem("user")
.then((userString) => {
if (userString) {
console.log("logged in.");
setSteamID(JSON.parse(userString));
fetchUserProfile(steamID.id).then((response) => {
setProfileInfo(response);
});
} else {
console.log("not logged in, showing LoginPage");
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
}, []);
return (
<>
<Button
title="btn"
onPress={() => {
navigation.navigate("LoginScreen")
}}
></Button>
export default UserProfile;
Inside the BottomTabNavigator you can check if the user is logged in. You can get the user in the same way you're getting it in UserProfile.tsx file.
export const BottomTabNavigator = () => {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
// check for user
}, []);
return (
<Tab.Navigator
screenOptions={{ ... }}
>
<Tab.Screen
name="LoginRoutes"
component={user ? UserScreen : LoginScreen}
/>
</Tab.Navigator>
);
};
Alternatively, you can look into getting the user through Context Provider so that you don't have to check the storage every time you want to see if the user is logged in.
Read more about that here:
How To Manage User State with React Context
React Context

invalid hook call in mobx+react native

I'm new to mobx,
I was told that I can't use directly rootStore from rootStore.tsx directly, and I have to replace it with hook, so I've tried to call hook useStore from rootStore.tsx
but in this case I've got an error "invalid hook call. Hooks can be called inside of the body"
my files are:
rootStore.tsx
import { createContext, useContext } from 'react'
import { makeAutoObservable } from 'mobx'
import { AsyncTrunk } from 'mobx-sync'
import AsyncStorage from '#react-native-async-storage/async-storage'
import { DayStyle, firstDayStyle } from '../styles/markedDayStyle'
const period: Record<string, DayStyle> = {
'2022-02-16': firstDayStyle,
}
export const rootStore = makeAutoObservable({
periods: period,
})
export const trunk = new AsyncTrunk(rootStore, {
storage: AsyncStorage,
})
export const StoreContext = createContext(rootStore)
export const StoreProvider = StoreContext.Provider
export const useStore = () => useContext(StoreContext)
App.tsx
const App = observer(({}) => {
const store = useStore()
const [isStoreLoaded, setIsStoreLoaded] = useState(false)
useEffect(() => {
const rehydrate = async () => {
await trunk.init()
setIsStoreLoaded(true)
}
rehydrate().catch(() => console.log('problems with localStorage'))
}, [store])
if (!isStoreLoaded) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator size="large" />
</View>
)
} else {
return (
<StoreProvider value={store}>
<PaperProvider theme={store.isDarkMode ? darkTheme : lightTheme}>
<View style={styles.container}>
<CalendarScreen/>
</View>
</PaperProvider>
</StoreProvider>
)
}
})
CalendarScreen.tsx
const CalendarScreen = observer(({}) => {
const store = useStore()
const handleDayPress = (day: DateData) => {
setModalVisible(true)
setPressedDay(day.dateString)
}
return (
<SafeAreaView style={styles.screenContainer}>
<Calendar
onDayPress={day => {handleDayPress(day)}}
/>
<View>
<ModalConfirmDay modalVisible={modalVisible} setModalVisible={setModalVisible} pressedDay={pressedDay} />
</View>
</SafeAreaView>
)
)}
ModalConfirmDay.tsx
import { fillMarkedDays } from '../functions/fillMarkedDays'
const ModalConfirmDay = observer(({ modalVisible, setModalVisible, pressedDay }: ModalConfirmDayProps) => {
const handlePeriodStarts = () => {
fillMarkedDays(pressedDay)
setModalVisible(false)
}
return (
<View style={styles.centeredView}>
<Modal
visible={modalVisible}
>
<View style={styles.modalView}>
<TouchableOpacity onPress={() => handlePeriodStarts()}>
<Text>Period starts</Text>
</TouchableOpacity>
</View>
</Modal>
</View>
)
})
fillMarkedDays.tsx
import { rootStore, useStore} from '../store/rootStore'
import { firstDayStyle} from '../styles/markedDayStyle'
const fillMarkedDays = (selectedDay: string) => {
const store = useStore()
if (selectedDay) {
store.periods[selectedDay] = firstDayStyle
}
}
when I try to add a new key-value (in fillMarkedDays.tsx) to store.periods I'm getting this
how can I fix this or select a better approach to call the store? Thanks everyone
By the rules of hooks you can't use hooks outside of the body of the function (component), so basically you can only use them before return statement, and also you can't use any conditions and so on. fillMarkedDays is just a function, not a component, it has no access to React context, hooks or whatever.
What you can do is first get the store with hook, then pass it as an argument into the fillMarkedDays function:
const ModalConfirmDay = observer(({ modalVisible, setModalVisible, pressedDay }: ModalConfirmDayProps) => {
const store = useStore()
const handlePeriodStarts = () => {
fillMarkedDays(store, pressedDay)
setModalVisible(false)
}
// ...
}

Render after fetching async data

I am fetching data in useEffect() and then modifying the object (modifying clients.unreadMessages based on what should render icon), which is later sent to the component to render. But this component does not render correctly always, the icon is sometimes missing. I think it's because data are modified after rendering.
ClientList
import Colors from '#helper/Colors';
import { useSelector } from '#redux/index';
import { HealthierLifeOption } from '#redux/types';
import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, ActivityIndicator } from 'react-native';
import ClientItem from './ClientItem';
import {usePubNub} from "pubnub-react";
export type Client = {
id: number;
username: string;
individualPlanPaid: boolean;
healthierLifeOption?: HealthierLifeOption;
company?: {
id: number;
companyName: string;
};
mentor?: {
id: number;
username: string;
};
}
type Props = {
navigation: any;
clients: Client[];
isAdmin?: boolean;
isLoading: boolean;
onEndReached: Function;
fromAdminTab?: boolean
}
const ClientList = ({ navigation, clients, onEndReached, isLoading, isAdmin = false, fromAdminTab= false }: Props) => {
const resultCount = useSelector(state => state.user.menteesResultCount);
const [hasMoreResults, setHasMoreResults] = useState(true);
const userId = useSelector(state => state.user.id);
const pubnub = usePubNub();
useEffect(() => {
setHasMoreResults(resultCount !== clients?.length);
}, [resultCount, clients]);
useEffect(() => {
getUnreadMessagesProccess().then(r => console.log("aaaaaaaaa"));
}, []);
async function setGrant() {
return new Promise( async resolve => {
await pubnub.grant({
channels: [userId.toString() + '.*'],
ttl: 55,
read: true,
write: true,
update: true,
get: true,
}, response => resolve(response));
});
}
async function getMetadata() {
const options = {include: {customFields: true}};
return await pubnub.objects.getAllChannelMetadata(options);
}
function setChannelsAndTokens(channelMetadata, channels, tokens) {
channelMetadata.data.forEach((value, index) => {
tokens.push(value.custom.lastToken);
channels.push(value.id)
});
}
async function getUnreadedMessages(channels, tokens) {
return await pubnub.messageCounts({
channels: channels,
channelTimetokens: tokens,
});
}
async function getUnreadMessagesProccess() {
const tokens = ['1000'];
const channels = ['1234567'];
const auth = await setGrant();
const channelMetadata = await getMetadata();
const l = await setChannelsAndTokens(channelMetadata, channels, tokens);
const unread = await getUnreadedMessages(channels, tokens).then((res) => {
clients.forEach((value, index) => {
if (res.channels[value.id + '-' + userId + '-chat']) {
value.unreadMessages = res["channels"][value.id + '-' + userId + '-chat'];
} else {
value.unreadMessages = 0;
}
})
console.log(res);
});
return unread;
}
return (
<View>
<FlatList
keyExtractor={item => item.id.toString()}
data={clients}
onEndReached={() => hasMoreResults ? onEndReached() : null}
onEndReachedThreshold={0.4}
renderItem={item => (
<ClientItem
client={item.item}
isAdmin={isAdmin}
navigation={navigation}
fromAdminTab={fromAdminTab}
/>
)}
ListFooterComponent={isLoading
? () => (
<View style={{ padding: 20 }}>
<ActivityIndicator animating size="large" color={Colors.border_gray} />
</View>
)
: null
}
/>
</View>
);
}
export default ClientList;
ClientItem
import React, {useEffect, useState} from 'react';
import { Text, View, Image, TouchableOpacity } from 'react-native';
import Images from '#helper/Images';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import Colors from '#helper/Colors';
import styles from '../styles';
import ClientModal from './ClientModal/ClientModal';
import { Client } from './ClientList';
import { HealthierLifeOption } from '#redux/types';
type Props = {
client: Client;
navigation: any;
isAdmin?: boolean;
fromAdminTab?: boolean
}
const ClientItem = ({ client, navigation, isAdmin = false, fromAdminTab= false }: Props) => {
const [showModal, setShowModal] = useState(false);
const [showIcon, setShowIcon] = useState(false)
console.log(client.unreadMessages)
useEffect(() =>{
if(client.unreadMessages>0)
setShowIcon(true);
},[client.unreadMessages]);
let clientIcon = Images.icon.logoIcon;
const handleContinueButton = () => {
if(!fromAdminTab) {
navigation.navigate('ChatFromClientsList', { selectedId: client.id, showBackButton: true });
}else {
setShowModal(!showModal)
}
};
return (
<View style={styles.client_item_container}>
<TouchableOpacity style={styles.client_content_container} onPress={handleContinueButton}
>
<View style={styles.image_container}>
<Image
style={styles.client_profile_img}
source={clientIcon}
resizeMode="contain"
resizeMethod="resize"
/>
</View>
<View style={styles.title_container}>
<Text style={styles.title} >{ client.username } </Text>
</View>
<View style={styles.dot_container}>
{showIcon && <FontAwesome5
name="comment-dots"
size={20}
color={Colors.red}
/> }
</View>
<View style={styles.hamburger_container} >
<FontAwesome5
name="bars"
size={30}
color={Colors.black}
onPress={() => setShowModal(!showModal)}
/>
</View>
<View>
<ClientModal
isVisible={showModal}
onCollapse={(value: boolean) => setShowModal(value)}
closeModal={(value: boolean) => setShowModal(value)}
client={client}
navigation={navigation}
isAdmin={isAdmin}
/>
</View>
</TouchableOpacity>
</View>
);
};
export default ClientItem;
This code does not render correctly always:
{showIcon && <FontAwesome5
name="comment-dots"
size={20}
color={Colors.red}
/> }
You should not calculate the value in renderItem.
Why you don’t calc the value outside of renderItem in pass it in
useEffect(() =>{
if(client.unreadMessages>0)
setShowIcon(true);
},[client.unreadMessages]);
Do something like:
renderItem={item => (
<ClientItem
client={item.item}
isAdmin={isAdmin}
navigation={navigation}
fromAdminTab={fromAdminTab}
showIcon={item.item.unreadMessages>0}
/>
)}
By the way renderItem should not be a anonymous function.

× TypeError: navData.navigation.toggleDrawer is not a function v4x

I am following a react native course. I am trying to create an iconName = "ios-menu" but when trying to open it it does not work and it tells me that "TypeError: navData.navigation.toggleDrawer is not a function" I can't find a solution I would like to know why this happens somebody could help me.
enter code here
import { HeaderButtons, Item } from 'react-navigation-header-buttons';
import { CATEGORIES } from '../data/dummy-data';
import HeaderButton from'../components/HeaderButton';
import CategoryGridTile from '../components/CategoryGridTile';
import { DrawerActions } from 'react-navigation-drawer';
import { createDrawerNavigator } from 'react-navigation-drawer';
const CategoriesScreen = props => {
const renderGridItem = itemData => {
return (
<CategoryGridTile
title={itemData.item.title}
color={itemData.item.color}
onSelect={() => {
props.navigation.navigate({
routeName: 'CategoryMeals',
params: {
categoryId: itemData.item.id
}
});
}}
/>
);
};
return (
<FlatList
keyExtractor={(item, index) => item.id}
data={CATEGORIES}
renderItem={renderGridItem}
numColumns={2}
/>
);
};
CategoriesScreen.navigationOptions = navData => {
return {
headerTitle: 'Meal Categories',
headerLeft: (
<HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item
title="Menu"
iconName="ios-menu"
onPress={() => {
navData.navigation.toggleDrawer();
}}
/>
</HeaderButtons>
)
};
};
In your DrawerNavigator.js when you create it, Probably you didn't export the module correctly at export default createAppContainer(DrawerNavigator "not stack but drawer");
Probably the reason why the toggleDrawer() function wasn't recognised as the react-navigation-drawer methods wasn't passed to App.js

Navigate to another page passing variables

I'm very new to React Native. I've got the following page which loads dates and results from a database, all displaying ok:
import React, { useEffect, useState } from "react";
import {
Text,
FlatList,
StyleSheet,
TouchableOpacity,
Platform,
} from "react-native";
import AsyncStorage from "#react-native-community/async-storage";
import Colors from "../../../res/Colors/Colors";
import { s, scale } from "react-native-size-matters";
import Axios from "axios";
import moment from "moment";
import { SafeAreaView} from "react-navigation";
export default function HistoryScreen({ navigation }) {
const [userDetails, setUserDetails] = useState(null);
const [loading, setLoading] = useState(true);
const [Star, setStar] = useState(null);
const [people, setPeople] = useState(null);
useEffect(() => {
_getHistory();
}, []);
const expandMonth = (month,year) => {
console.log(month);
console.log(year);
}
const _getHistory = async () => {
let star = await AsyncStorage.getItem("star");
star = JSON.parse(star);
setStar(star);
var formData = new FormData();
formData.append("StarID", star.StarID);
Axios({
method: "post",
url: "**API URL**",
data: formData,
//headers: { "Content-Type": "multipart/form-data" },
})
.then(async (response) => {
var responseBody = response.data.detail;
setPeople(responseBody);
setLoading(false);
})
.catch((error) => {
console.log({ error });
});
};
if (loading)
return (
<SafeAreaView>
</SafeAreaView>
);
return (
<SafeAreaView
style={{
flex: 1,
backgroundColor: Colors.lightGrey,
paddingTop: Platform.OS === "android" ? 40 : 40,
justifyContent: "space-evenly",
}}
>
<Text style={styles.headerText}>History</Text>
<FlatList
//keyExtractor={(item) => item.id}
data={people}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => {
expandMonth(item.Month,item.Year)
navigation.navigate("HistoryMonthScreen", {
Month: item.Month,
Year: item.Year
});
}}
style={styles.month}>
<Text style={styles.monthname}>{moment(item.Month, 'M').format('MMMM')} {item.Year}</Text>
<Text style={styles.monthcount}>{item.Count} Bookings</Text>
</TouchableOpacity>
)}
/>
</SafeAreaView>
);
}
However when clicking the TouchableOpacity object it doesn't navigate to the next page passing the year and month variables. I assume I need to call some form of navigation to it, however everything I have tried has resulted in error after error.
For React Navigation 5 you have to use the route prop that is passed in alongside navigation for whatever screen you're navigating to.
Put the below code into HistoryMonthScreen.js
const HistoryMonthScreen = ({route, navigation}) => {
console.log(route.params.Month, route.params.Year);
return <View />
}
Edit: For context:
I'm going off what you have here
navigation.navigate("HistoryMonthScreen", {
Month: item.Month,
Year: item.Year
});
So I'm assuming you have some React Navigator somewhere that looks like this:
const AppNavigator5 = () => {
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName="HistoryScreen"
screenOptions={{
headerShown: false,
animationEnabled: false,
}}>
<Stack.Screen
name="HistoryScreen"
component={HistoryScreen}
/>
<Stack.Screen
name="HistoryMonthScreen"
component={HistoryMonthScreen}
/>
<Stack.Screen name="Egg" component={Egg} />
</Stack.Navigator>
</NavigationContainer>
);
};
You put the code with route and navigation inside that HistoryMonthScreen component. You can pass different params into route.params every time, you don't have to change HistoryMonthScreen.