Handle component remounting with react-navigation (same implementation, different result) - react-native

I've got a problem which I cannot seem to solve.
I have an app built with react-native and expo and react-navigation.
What I am trying to achieve:
A user can sign up for an event. All events the user wants to participate are listed on one screen. Pressing on one marks this event as "active" - The user can (on another screen) now see details and a bulletin board for this event.
Hierarchy:
- Loginstack (not relevant, just for completion)
- Homestack
-- Home
-- Eventstack (Active Event)
--- Details
--- Bulletin Board
-- Create Event
-- Sign up for Event
-- List Events User goes to
From the Sign up Screen I navigate to the Eventstack while first add the Event to local storage. The Details Screen now checks for the active event and loads the data. When the user navigates away from this screen and back to it, the Details screen should only reload the data if the active event has changed! First, the Stack Navigation:
<Stack.Navigator initialRouteName="EventDetails" headerMode="none" screenOptions={({ route, navigation}) => ({
animationEnabled: false,
})}>
<Stack.Screen
name="EventDetails"
component={EventDetailsScreen}
initialParams={{ active: EventFooter.details, }}
/>
<Stack.Screen
name="Bulletin"
component={BulletinScreen}
initialParams={{ active: EventFooter.bulletin, }}
/>
</Stack.Navigator>
My Code:
const EventDetailsScreen = ({ navigation }) => {
const [isLoading, setLoading] = useState(true);
const [eventToLoad, setEventToLoad] = useState(null);
const [event, setEvent] = useState(null);
const userData = useContext(UserContext);
const _loadEvent = async (eId?: number) => {
setLoading(true);
let eventId: number = eId || await getActiveEvent();
if (eventId == null) {
if (userData.user.event.id === undefined) {
return;
}
eventId = userData.user.event.id;
}
const eventToSet = await findEventById(eventId);
setEvent(eventToSet);
setLoading(false);
};
const _checkEvent = async () => {
const actEvent = await getActiveEvent();
setEventToLoad(actEvent);
}
useFocusEffect(
useCallback(() => {
_checkEvent();
}, [])
);
useEffect(() => {
_loadEvent();
}, [eventToLoad]);
return ( ... );
};
For this code the following is happening. The first time the user navigates to the Details Screen the Event Details are loaded from the server via useEffect. When I now navigate away and back to this screen useFocusEffect would set the Active Event Id. If it is the same as before, useEffect will not be fired... Working. Logs are saying the same. The first time the active event is set, for all next navigations to the details sreen the already set data is used. Now for the Bulletin Board (same Hierarchy level) I implemented it the same way.
const BulletinScreen = ({ navigation }) => {
const [isLoading, setLoading] = useState(false);
const [eventToLoad, setEventToLoad] = useState(null);
const [threads, setThreads] = useState<Array<Thread>>(null);
const userData = useContext(UserContext);
const _getThreads = async () => {
setLoading(true);
let eventId: number = await getActiveEvent();
if (eventId == null) {
if (userData.user.event.id === undefined) {
return;
}
eventId = userData.user.event.id;
}
const threadsToSet = await getThreads(eventId, userData.user);
setThreads(threadsToSet);
setLoading(false);
};
const _getEvent = async () => {
const actEvent = await getActiveEvent();
setEventToLoad(actEvent);
}
useFocusEffect(
useCallback(() => {
_getEvent();
}, [])
);
useEffect(() => {
_getThreads();
}, [eventToLoad]);
return ( ... );
};
This time, every time I navigate to the bulletin screen the initial states (null) are used and therefore trigger a reload, which is not what I want.
I use a stack navigation and native-base for a footer tab navigation, which is used to navigate between the Details and Bulletin Screen. Do I have a blind thinking spot? It is the same implementation, right? Why does it behave differently?

Follow-up: Stack Navigation just works that way... Tab Navigation does not. So it works with a Tab Navigation.

Related

Expo-notifications trigger all useEffects in the application

I created the entire flow for expo-notifications, although I encounter one problem. Once I receive the notification, the UI of the specific type is re-rendered and - which is the core of the problem - all the useEffects with fetch get triggered in the application; it seems that it re-renders the entire application. Even disabling the update of the specific part of the UI (that I want to update) still causes that a notification makes the app to re-render.
I tried to find the cause of that, but no progress so far. Did anyone of you ever encountered this kind of problem? Why the app gets re-rendered entirely?
The function registerForPushNotificationsAsync is copy-pasted from their docs.
Here is my notification provider - I get notification correctly, but idk what causes the re-render and trigger all the useEffects:
const NotificationsProvider = () => {
const authenticationStatus = useSelector(authStatus);
const dispatch = useDispatch();
const [expoPushToken, setExpoPushToken] = useState("");
const [notification, setNotification] = useState<Notifications.Notification | null>(null);
useEffect(() => {
if (authenticationStatus === AUTHENTICATION_MESSAGES.AUTHENTICATION_SUCCESS) {
registerForPushNotificationsAsync()
.then((token) => setExpoPushToken(token))
.catch((error) => console.error(error));
const subscription = Notifications.addNotificationReceivedListener((receivedNotification) => {
setNotification(receivedNotification);
const { id, title } = receivedNotification.request.content.data;
console.log(receivedNotification.request.content.data);
dispatch(
addAsync(
[
{
id: id,
title: title,
},
],
1 * 1000
)
);
});
APP.tsx
const App = () => {
const [fontsLoaded] = useFonts({
Roboto_400Regular,
Roboto_500Medium,
});
return fontsLoaded ? (
<Provider store={store}>
<PaperProvider theme={theme}>
<NotificationsProvider />
</PaperProvider>
</Provider>
) : (
<AppLoading />
);
};

React native disable timer in bottom navigation not working

I have 4 screens in bottom navigation.
The first screen consists of a map with the timer of 10s . As soon as the timer get executed the api is hit.
But when a user switch the screen with bottom navigation tab item . The timer still works in background and due to which the other api start having lag.
How to make sure the timer only works when that screen is focused?
I tried updating the name of screen user is navigating using useContext however when the timer execute it do not return the update name of the screen. every time it returns the older screen name.
This code is in all the 4 bottom navigation screens. As I have observed useEffect only works once. and whenever user clicks it second time this hook do not get trigger.
HOME SCREEN
const {activeScreenFun, activeScreen, previousScreen} = useNavigationCustom();
React.useEffect(() => {
const unsubscribe = navigation.addListener('tabPress', e => {
activeScreenFun('Home');
});
return unsubscribe;
}, [navigation]);
useEffect(() => {
activeScreenFun('Home');
}, []);
Timer
useEffect(() => {
if (timer) {
let interval = setInterval(() => {
getAPiData();
}, 10000);
return () => {
clearInterval(interval);
};
}
}, [timer]);
NavigationCustomProvider Context
export function NavigationCustomProvider({children}) {
const [activeScreen, setActiveScreen] = useState('');
const [previousScreen, setPreviousScreen] = useState('');
const activeScreenFun = useCallback(async function (activeScreenSelected) {
setPreviousScreen(activeScreen);
setActiveScreen(activeScreenSelected);
});
const getActiveScreenFun = useCallback(() => {
return activeScreen;
});
Bottom Navigation Code
export default function MainScreen() {
return (
<NavigationCustomProvider>
<MainLayout>
<MainLayoutScreen
name={HOME_ROUTE}
icon={TrackItIcon}
activeIcon={TrackItActiveIcon}
component={HomeScreen}
/>
<MainLayoutScreen
name={ATTENDACE_ROUTE}
icon={AttendanceIcon}
activeIcon={AttendanceActiveIcon}
component={AttendanceScreen}
/>
<MainLayoutScreen
name={NOTIFICATION_ROUTE}
icon={NotificationIcon}
activeIcon={NotificationActiveIcon}
component={NotificationScreen}
/>
<MainLayoutScreen
name={MY_ACCOUNT_ROUTE}
icon={AccountIcon}
activeIcon={AccountActiveIcon}
component={ProfileScreen}
/>
</MainLayout>
</NavigationCustomProvider>
);
}
TAB BAR CODE
routes = children.map(x => ({
name: x.props.name,
icon: x.props.icon,
activeIcon: x.props.activeIcon,
component: x.props.component,
}));
<Tab.Navigator
barStyle={{backgroundColor: theme.colors.white}}
activeColor={theme.colors.primary}
shifting={true}
labeled={true}>
{routes.map(x => {
let Icon = x.icon;
let ActiveIcon = x.activeIcon;
return (
<Tab.Screen
key={x.name}
name={x.name}
component={x.component}
options={{
tabBrColor: theme.colors.white,
tabBarIcon: ({focused}) =>
focused ? <ActiveIcon /> : <Icon />,
}}
/>
);
})}
</Tab.Navigator>
A new timer instance is created for each new component rerender.
Even if you clear an instance of the timer when the component unmounts, previously created instances are still running in the background.
you need to persist a single instance of timer across all component rerender cycles.
React provide hook useRef to persist value for all component render cycle.
let interval = React.useRef(null);
useEffect(() => {
if (timer) {
// Assign and persist Timer value with ref
interval.current = setInterval(() => {
getAPiData();
}, 10000);
return () => {
if (interval.current) {
clearInterval(interval);
}
};
}
}, [timer]);

How to pass data from async function to Drawer Navigator in React Native

im a new to react native but trying to build my own application.
I'm trying to pass storeKey and userName obtained from DB to CustomDrawer and Drawer.Screen so I don't need to repeat the function everytime.
so, it's working inside HomeScreen, console.log prints proper value. However, when I pass it in Drawer.Screen 'stock', and then print it in that module, it shows empty array.
How can I pass value from async to drawer navigator properly?
and how can I pass it to CustomDrawer? will {...props} already contain the value?
When I print props.params in the CustomDrawer module, it only says undefined..
const HomeScreen = ({ navigation }) => {
const [storeKey, setStoreKey] = useState([]);
const [userName, setName] = useState([]);
useEffect(async() => {
let isMounted = true;
if(isMounted){
console.log('zzzz')
const auth = getAuth();
const user = auth.currentUser;
if(user !== null){
const email = user.email;
const UserInfo = await getDoc(doc(db, 'users', email));
if(UserInfo.exists()){
setName(UserInfo.data().name);
setStoreKey(UserInfo.data().storeKey)
return () => {
isMounted = false
}
}
else{
console.log('None')
}
}
}
}, [storeKey]);
console.log('this',storeKey)
return (
<Drawer.Navigator drawerContent={props => <CustomDrawer {...props} />} screenOptions={headerStyles} initialRouteName={HomeScreen} >
<Drawer.Screen name='Search' component={SearchScreen} options={QuitIcon}/>
<Drawer.Screen name='Stock' component={StockScreen} options={QuitIcon} initialParams={{storeKey: storeKey}}/>
</Drawer.Navigator>
)
}
Even if the UseEffect is async or none the following code will be the same,
when calling the console.log('this', storeKey) the data is not yet in the state, to wait the useEffect run before continue the code, you have to add an empty array as a second argument in the useEffect function like this :
useEffect(() => {
}, []) // <-- empty array here
by this way your useEffect will be run only the first render of the app and the program will wait the useEffect to be run before continue.

RN OneSignal _open Event

OneSignal on notification open event fires after the home screen got launched then it navigates to the desired screen. I want to detect if the app was launched on pressing the notification prior the home screen get rendered so I can navigate to the Second screen directly and avoid unnecessarily calling of apis.
"react-native-onesignal": "^3.9.3"
"react-navigation": "^4.0.0"
code
const _opened = openResult => {
const { additionalData, body } = openResult.notification.payload;
// how to navigate or set the initial screen depending on the payload
}
useEffect(() => {
onesignal.init();
onesignal.addEventListener('received', _received);
onesignal.addEventListener('opened', _opened);
SplashScreen.hide();
return () => {
// unsubscriber
onesignal.removeEventListener('received', _received);
onesignal.removeEventListener('opened', _opened);
}
}, []);
Debug
your question is how to navigate or set the initial screen depending on the opened notification payload?
1) - set the initial screen depending on the opened notification payload.
according to class Lifecycle useEffect runs after the component output has been rendered, so listener in useEffect not listen until the component amounting, and this the reason of logs in home screen shown before logs in useEffect, see this explanation.
//this the problem (NavigationContainer called before useEffect).
function App() {
useEffect(() => {}); //called second.
return <NavigationContainer>; //called first.
}
//this the solution (useEffect called Before NavigationContainer).
function App() {
const [ready, setReady] = useState(false);
//called second.
useEffect(() => {
//listen here
setReady(true);
SplashScreen.hide();
});
//called first
//no function or apis run before useEffect here it just view.
if(!ready) return <></>;// or <LoadingView/>
//called third.
return <NavigationContainer>;
}
your code may be like this.
function App() {
const [ready, setReady] = useState(false);
const openedNotificationRef = useRef(null);
const _opened = openResult => {
openedNotificationRef.current = openResult.notification.payload;
}
const getInitialRouteName = () => {
if (openedNotificationRef.current) {
return "second"; //or what you want depending on the notification.
}
return "home";
}
useEffect(() => {
onesignal.addEventListener('opened', _opened);
//setTimeout(fn, 0) mean function cannot run until the stack on the main thread is empty.
//this ensure _opened is executed if app is opened from notification
setTimeout(() => {
setReady(true);
}, 0)
});
if(!ready) return <LoadingView/>
return (
<NavigationContainer initialRouteName={getInitialRouteName()}>
</NavigationContainer>
);
}
2) - navigate depending on the opened notification payload.
first you need to kown that
A navigator needs to be rendered to be able to handle actions If you
try to navigate without rendering a navigator or before the navigator
finishes mounting, it will throw and crash your app if not handled. So
you'll need to add an additional check to decide what to do until your
app mounts.
read docs
function App() {
const navigationRef = React.useRef(null);
const openedNotificationRef = useRef(null);
const _opened = openResult => {
openedNotificationRef.current = openResult.notification.payload;
//remove loading screen and start with what you want.
const routes = [
{name : 'home'}, //recommended add this to handle navigation go back
{name : 'orders'}, //recommended add this to handle navigation go back
{name : 'order', params : {id : payload.id}},
]
navigationRef.current.dispatch(
CommonActions.reset({
routes : routes,
index: routes.length - 1,
})
)
}
useEffect(() => {
//don't subscribe to `opened` here
//unsubscribe
return () => {
onesignal.removeEventListener('opened', _opened);
}
}, []);
//subscribe to `opened` after navigation is ready to can use navigate
const onReady = () => {
onesignal.addEventListener('opened', _opened);
//setTimeout(fn, 0) mean function cannot run until the stack on the main thread is empty.
//this ensure _opened is executed if app is opened from notification
setTimeout(() => {
if (!openedNotificationRef.current) {
//remove loading screen and start with home
navigationRef.current.dispatch(
CommonActions.reset({
routes : [{name : 'home'}],
index: 0,
})
)
}
}, 0)
};
return (
<NavigationContainer
ref={navigationRef}
onReady={onReady}
initialRouteName={"justLoadingScreen"}>
</NavigationContainer>
);
}
refrences for setTimeout, CommonActions.

How to receive one callback from three simultaneous events in React Native?

I'm listening to three different events that happen simultaneously and I want to get some data from each of these events. However, I want to receive the latest data from all three events at once.
I tried using useEffect but, of course, this is triggering the callback at least three times, instead of just once.
const [key, setKey] = useState('');
const [text, setText] = useState('');
const [position, setPosition] = useState(0);
const onKeyPress = ({ nativeEvent: { key } }) => setKey(key);
const onChangeText = text => setText(text);
const onSelectionChange = ({ nativeEvent: { selection: { start, end } } }) => {
start === end && setPosition(start);
}
useEffect(() => {
// I want to do stuff with the latest key, text and position.
// However, this is called more than once when typing.
}, [key, text, position]);
// ..
<TextInput
onChangeText={onChangeText}
onKeyPress={onKeyPress}
onSelectionChange={onSelectionChange}
/>
How can I achieve this?