React native disable timer in bottom navigation not working - react-native

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]);

Related

React Navigation upgrade v4 to v6 - navigationOptions and listener

Previously with React Navigation v4, I have use route.params.scrollToTop() to pass the function I want to call when tabBarOnPress. The function content is not related to scrollToTop. After upgrading to React Navigation v6, I have changed the code. However, with all the changes, the scrollToTop function only called once. Once I switch to another Tab and go back to ScreenB, the componentDidMount do not trigger again. I would like to call the function every time I tap the tab just like how it respond in v4, no matter which screen I'm switching from.
v4
const RouteConfigs = {
ScreenA:{
screen: ComponentA,
},
ScreenB:{
screen: ComponentB,
navigationOptions: {
tabBarOnPress: (event) => {
const { navigation } = event;
event.defaultHandler();
if (navigation.isFocused() && navigation.state.params && navigation.state.params.scrollToTop) {
navigation.state.params.scrollToTop();
}
},
}
}
}
const Tabs = createBottomTabNavigator(RouteConfigs, TabNavigatorConfig)
Tabs.navigationOptions = ({navigation}) => {
const {routeName} = navigation.state.routes[navigation.state.index]
return{
header: null,
}
}
v6
Navigation
const Tab = createBottomTabNavigator();
<Tab.Navigator>
<Tab.Screen
name="ScreenA"
component={ComponentA}
>
<Tab.Screen
name="ScreenB"
component={ComponentB}
listeners={({ navigation, route, params}) => ({
tabPress: (e) => {
if (navigation.isFocused() && route.params && route.params.scrollToTop) {
route.params.scrollToTop();
}
},
})}
>
</Tab.Navigator>
I did not change anything in ComponentB
...
componentDidMount(){
this.props.navigation.setParams({
scrollToTop: this.scrollToTop,
});
}
scrollToTop = ()=>{
console.log("Press")
this.setState(()=> ({...}))
}
...
I am not sure is it because I did not add the part. However I have no idea how to add it under v6.
Tabs.navigationOptions = ({navigation}) => {
const {routeName} = navigation.state.routes[navigation.state.index]
return{
header: null,
}
}

Flatlist inside tab navigator is scrolling to top on state change in react native

Here you can see the gif
Here is my whole Navigator functional component. I'm trying to implement two tabs using Tab Navigator. One to display the cryptos and the other to display the forex data.
The problem is, when I try to load more data on reaching the flatlist's end, the flatlist is scrolling to the top since I'm making a state change [page+1].
const Navigator = () => {
const Tab = createMaterialTopTabNavigator();
const renderItems = ({ item }) => (
<Text>{item.name}<Text>
);
const fetchMarketData = async () => {
console.log("Fetching");
const marketData = await getCryptoMarketData({ page });
if (marketData != "Network Error") {
const ids = data.map((item) => item.id);
let newData = marketData.filter((item) => !ids.includes(item.id));
setData([...data, ...newData]);
setFetching(false);
} else {
setFetching(false);
Alert.alert(marketData, "Sorry for the inconvenience");
}
};
useEffect(() => {
setFetching(true);
const data = async () => {
await fetchMarketData();
};
}, [page]);
const handleLoadMore = async () => {
setFetching(true);
setPage((page) => page + 1);
};
const ScreenA = () => (
<FlatList
data={data}
style={{ backgroundColor: "white" }}
keyExtractor={(item) => item.id}
renderItem={renderItems}
scrollEventThrottle={16}
onEndReached={handleLoadMore}
onEndReachedThreshold={0}
/>
);
return (
<Tab.Navigator
screenOptions={({ route }) => screenOptions(route)}
keyboardDismissMode="auto"
>
<Tab.Screen name="Crypto" component={ScreenA} />
<Tab.Screen name="Forex" component={ScreenC} />
</Tab.Navigator>
);
};
export default Navigator;
OnEndReached is firing the handleLoadMore function and after the state change on data, the Flatlist is scrolling to the top.
1st reason
you have typo in "fetchMarketData", how exactly u get "newData" because i cant see it anywhere, maybe it should be "marketData" if not then u adding SAME old data PLUS undefined[...data, ...undefined]
2nd reason
reason why is that u call setPage(page + 1) and then "fetchMarketData" this is bad why ? because setState is async and it can be changed instant or after 5 secound, so u dont know when its changed and this is why we have hooks, you can use "useEffect" to handle this
change your "handleLoadMore" for example like this
const handleLoadMore = () => {
setPage(page + 1);
};
add useEffect hook that runs when "page" state changes
React.useEffect(() => {
(async() => {
setFetching(true)
const marketData = await getCryptoMarketData({ page });
if (marketData != "Network Error") {
setData([...data, ...marketData]);
} else {
Alert.alert(marketData, "Sorry for the inconvenience");
}
setFetching(false)
})()
}, [page])

How to hide element when device keyboard active using hooks?

I wanted to convert a hide element when keyboard active HOC I found to the newer react-native version using hooks (useEffect), the original solution using the older react lifecycle hooks looks like this - https://stackoverflow.com/a/60500043/1829251
So I created a useHideWhenKeyboardOpen function that wraps the child element and should hide that child if the device keyboard is active using useEffect. But on render the child element useHideWhenKeyboardOpen isn't displayed regardless of keyboard displayed.
When I've debugged the app I see the following error which I didn't fully understand,because the useHideWhenKeyboardOpen function does return a <BaseComponent>:
ExceptionsManager.js:179 Warning: Functions are not valid as a React
child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than
return it.
in RCTView (at View.js:34)
Question:
How can you attach keyboard displayed listener to a component in the render?
Example useHideWhenKeyboardOpen function:
import React, { useEffect, useState } from 'react';
import { Keyboard } from 'react-native';
// Wrapper component which hides child node when the device keyboard is open.
const useHideWhenKeyboardOpen = (BaseComponent: any) => (props: any) => {
// todo: finish refactoring.....
const [isKeyboadVisible, setIsKeyboadVisible] = useState(false);
const _keyboardDidShow = () => {
setIsKeyboadVisible(true);
};
const _keyboardDidHide = () => {
setIsKeyboadVisible(false);
};
/**
* Add callbacks to keyboard display events, cleanup in useeffect return.
*/
useEffect(() => {
console.log('isKeyboadVisible: ' + isKeyboadVisible);
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
return () => {
Keyboard.removeCurrentListener();
};
}, [_keyboardDidHide, _keyboardDidShow]);
return isKeyboadVisible ? null : <BaseComponent {...props}></BaseComponent>;
};
export default useHideWhenKeyboardOpen;
Example Usage:
return(
.
.
.
{useHideWhenKeyboardOpen(
<View style={[styles.buttonContainer]}>
<Button
icon={<Icon name="save" size={16} color="white" />}
title={strings.STOCKS_FEED.submit}
iconRight={true}
onPress={() => {
toggleSettings();
}}
style={styles.submitButton}
raised={true}
/>
</View>,
)}
)
Mindset shift will help: think of hooks as data source rather than JSX factory:
const isKeyboardShown = useKeyboardStatus();
...
{!isKeyboardShown && (...
Accordingly your hook will just return current status(your current version look rather as a HOC):
const useHideWhenKeyboardOpen = () => {
const [isKeyboadVisible, setIsKeyboadVisible] = useState(false);
const _keyboardDidShow = useCallback(() => {
setIsKeyboadVisible(true);
}, []);
const _keyboardDidHide = useCallback(() => {
setIsKeyboadVisible(false);
}, []);
useEffect(() => {
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
return () => {
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
};
}, [_keyboardDidHide, _keyboardDidShow]);
return isKeyboadVisible;
};
Note usage of useCallback. Without it your hook will unsubscribe from Keyboard and subscribe again on every render(since _keyboardDidHide would be referentially different each time and would trigger useEffect). And that's definitely redundant.

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

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.

Performing action on swipe back (react-navigation)

In react-navigation, know there is a way to perform custom actions when the back button is pressed. Is there a similar way to do this when the user swipes to go back to the last screen?
I was able to determine whether the app is going back via the back button or swiping back with the getStateForAction function of the router. Play around with checking the action and state variables to fine tune it more if needed.
export const ScreensStack = StackNavigator(
{
Login: {screen: LoginScreen},
Schedule: {screen: ScheduleScreen},
}
/**
* Intercept actions occuring in the screen stack.
*/
const getStateForActionScreensStack = ScreensStack.router.getStateForAction;
ScreensStack.router = {
...ScreensStack.router,
getStateForAction(action, state) {
if (action.type == 'Navigation/BACK') {
console.log('We are going back...');
}
return getStateForActionScreensStack(action, state);
},
};
React Navigation 5.7 introduced a simple way to do this without the need to override back actions or swipe gestures, basically you intercept the back action in the component and execute a callback before it happens:
import {useNavigation} from '#react-navigation/native';
const MyScreenComponent = (props) => {
const navigation = useNavigation();
React.useEffect(() => (
navigation.addListener('beforeRemove', (e) => {
// Prevent default behavior of leaving the screen (if needed)
e.preventDefault();
// Do whatever...
})
), [navigation]);
// Rest of component
}
In this example I'm using the useNavigation hook to access the navigation object, but you can also extract it from the props, it really depends on how your app is built.
Official documentation.
Using this in react-navigation v5.0
So I am using Pan-Responder to handle swipe-back event in react-native iOS.
So how I achieved this:
in my Navigator.js I have added gestureEnabled: false functionality of the app.
const Funnel = createStackNavigator({
page1: Page1,
page2: Page2,
page3: Page3,
}, {
// headerMode: 'float',
defaultNavigationOptions: ({ navigation }) => ({
...TransitionPresets.SlideFromRightIOS,
gestureEnabled: false,
header:
(<SafeAreaView style={{ flex: 1 }} forceInset={{ bottom: 'never' }}>
<AppHeader pageData={navigation.state.params == undefined ? data : navigation.state.params} />
</SafeAreaView>),
}),
})
so by above code I was able to remove back-swipe functionality in iOS app.
now I have to add swipe-back functionality.
export let panResponder = (callback) => PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
return Math.abs(gestureState.dx) > 5;
},
onPanResponderRelease: (evt, gestureState) => {
if (Platform.OS != 'ios') return
if (Math.floor(gestureState.moveX) >= 0 && Math.floor(gestureState.moveX) <= width / 2) {
callback()
}
},
});
so by using above code you will get a callback of swipe-back event in which you can add navigation.goBack() functionality.
How to add PanResponser in you view:
<View {...panResponder(backButtonCallback).panHandlers}/>
There are some examples in the react-navigation docs:
https://reactnavigation.org/docs/en/routers.html
I found that there is an immediate param on the action that is set true when the back is from swipe yet undefined in other types of back:
const MyApp = createStackNavigator({
Home: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
}, {
initialRouteName: 'Home',
})
const defaultGetStateForAction = MyApp.router.getStateForAction;
MyApp.router.getStateForAction = (action, state) => {
if (state && action.type === 'Navigation/BACK' && action.immediate) {
console.log('Perform custom action on swipe')
}
return defaultGetStateForAction(action, state);
};