How to reload a component in a bottom tab navigator - reactjs

I have 4 bottom tab navigator options
<Tab.Screen name="Home" component={HomeScreen} options={{headerShown:false}}/>
<Tab.Screen name="Appointments" component={Doctors}/>
<Tab.Screen name="Lab Tests" component={Diagnostics}/>
<Tab.Screen name="Account" component={Profile} options={{ unmountOnBlur: true }}
In the Account tab, I am showing the Profile Details And Edit Profile option.
Clicking on Edit Profile I go to Edit Profile Page, Edit Save,
const Save = navigation.navigate("Account")
After hitting Save, I am returned to the Account tab but the component Profile, which I am using as an Account tab component, is not reloading, so the Profile Details I am using are still the old Details.
As You Can see I already used unmountOnBlur : true, It works only when I am switching tabs, I want the same behavior when I came back from the Edit Profile page to the Account Tab.

If your getting the profile data from an API's that means your calling the same API in the Account Tab as well. You can add an event listener and every time your screen get into focus you can call that apis and get the updated result.
Ref: https://reactnavigation.org/docs/navigation-events.
Example:
React.useEffect(() => {
const unsubscribe = props.navigation.addListener('focus', () => {
// call your api here
});
return unsubscribe;
}, [navigation]);

Without a complete example, it's hard to figure out the exact issue. Maybe the profile component is using the useEffect without deps will not call again.
You can Call a function when focused screen changes or re-render a screen with the useIsFocused hook -
useIsFocused - this will cause a re-render. May introduce unnecessary component re-renders as a screen comes in and out of focus.
import * as React from 'react';
import { Text } from 'react-native';
import { useIsFocused } from '#react-navigation/native';
function Profile() {
// This hook returns `true` if the screen is focused, `false` otherwise
const isFocused = useIsFocused();
......
}
Or Triggering an action with a focus event listener​, you can control the re-renders
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
// screen is focused
});
// Return the function to unsubscribe from the event so it gets removed on unmount
return unsubscribe;
}, [navigation]);
There's also a useFocusEffect.

Related

React JS page keeps refreshing when using the back button

I have 2 React JS pages (A & B), when I go from A->B and back to A, page A is refreshed every time. I was under the impression that page is not destroyed. All related questions on StackOverflow seems to be about the opposite problem.
The reason the page refreshes is because useEffect() is called when the back button is pressed despite using useState() to prevent this. I even tried replacing 'refresh' with a 'props.id' parameter (that never changes). See code below:
Here's my code to page A:
import { useHistory, useParams } from "react-router-dom";
import React, { useState, useEffect, useRef } from "react";
import { Link } from "react-router-dom";
export default function Test(props) {
const [refresh, setRefresh] = useState(false);
useEffect(() => {
console.log("useEffect called: "+refresh);
setRefresh(true);
},[refresh]);
return (
<>
Hello from Test
<Link to="/test2">Test me</Link>
</>
);
}
I'm using react-router-dom: "^5.1.2", and import { BrowserRouter as Router } from "react-router-dom"; in App.js and specified:
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route exact path="/test">
<Test id="1"/>
</Route>
<Route exact path="/test2">
<Test2 />
</Route>
.....
Does anyone know how to prevent useEffect() from being triggered when returning to page? The actual page A fetches using a REST call and display a long list of items and I do not want the page to refresh every time the user load page B to view item and then returns to the page.
You need to add a condition to useEffect.
If you only want to setRefresh to true if its false, then do something like:
useEffect(() => {
if(!refresh) setRefresh(true)
}, [refresh])
Since you are starting with const [refresh, setRefresh] = useState(false) and are not changing refresh anywhere else in the component, this will run once everytime the component loads (not renders).
If you want to run this once in the lifetime of the app and not the component, you need to persist the information outside the component, by either lifting the state up to a parent component and persisting the information is something like localstorage/sessionstorage.
You could then extract this information whenever your component loads and set the refresh state variable accordingly.
Let's say you just want to setRefresh to true once. Add this useEffect:
useEffect(() => {
let persistedRefresh
try {
persistedRefresh = !!JSON.parse(window.localstorage.getItem('THE_KEY_TO_REFRESH_VALUE'))
} catch(error) {
persistedRefresh = false
}
setRefresh(persistedRefresh)
}, [])
This useEffect will run whenever the component loads, and update the state variable, triggering the previous useEffect.
We also need to modify the previous useEffect:
useEffect(() => {
if(!refresh) {
setRefresh(true)
window.localstorage.setItem('THE_KEY_TO_REFRESH_VALUE', JSON.stringify(true))
}
}, [refresh])
In this useEffect we are updating the persisted value so that whenever the component loads,
it will check the persisted value,
refresh if needed, and
update the persisted value for the next loads.
This is how you do it without any extra dependencies.
I can see that you're importing the very useful useHistory prop, but not doing much with it. It can actually be used to check if a user is navigating to the page by using the back button. useHistory()'s action properly will tell you everything you need. If the back button was used, action will be "POP". So you can put some logic into your useEffect to check for that:
const history = useHistory();
React.useEffect(() => {
if (history.action === "POP")
console.log("Back button used. Not running stuff");
else console.log("useEffect called in home");
}, []);
Here is a Sanbox. And here you can actually test the sandbox code in a dedicate browser window: https://okqj3.csb.app/
Click the "About" link and then use the back button to go back to "Home", in the console you will see how the Home element's useEffect function catches it.
Solution 1 (Correct way)
Use Stateless components and have a common super state (Redux will be of great assistance), and bind you page/data to common state so even if the state changes, the page will always render the current state creating an illusion of page retaining the state (I used it to run large queries and store progress/result in redux so even if I open another page and come back then also I see query in progress or result).
However I am not really sure what your use case is.
Solution 2 (slightly wrong way)
Use React.memo,You can use it when you don't want to update a component that you think is static
For function Components:
const Mycomponents = React.memo(props => {
return <div>
No updates on this component when rendering, use useEffect to verify too
</div>;
});
You shouldn't be defining any method/functionality/dynamic calculation inside this kind of method just to avoid getting irregular data

How to find current screen/route in nested Tab.Navigator

I have a custom TabBar for a Tab.Navigator that needs to have a different action when one of the tabs is selected based on what the current route is in the Stack Navigator component for that tab.
How can I inspect the currently displayed Stack.Screen inside of this custom TabBar? Trying to use the getRoute hook only shows me the parent screen that hosts that Tab.Navigator.
<Tab.Navigator tabBar={(props) => <BottomTabBar {...props} />}>
<Tab.Screen
name="Home"
component={HomeStack}
initialParams={{showSuccess: route.params?.showSuccess}}
/>
<Tab.Screen name="Alternate" component={AlternateScreen} />
</Tab.Navigator>
I can't pass the value in using tabBarOptions as I don't know what the selected route would be when the tab bar is created.
I ended up keeping track of the current screen outside the scope of the navigator as the built in Tab Navigator did not keep a reference to nested screens in a Stack Navigator like I was hoping it would.
Only needing to determine a difference between two screens, my crude hack was to have a current screen reference in a context hook and update that value in a useEffect hook when the relevant screen was mounted.
In whatever your provider is, have a currentScreen const like this:
const currentScreen = useRef('Closet');
Then, in your screens, destructure that value from the provider in the useContext hook:
const { currentScreen } = useContext(MyContext);
Then, in the same screen update that currentScreen value in a useEffect hook:
useEffect(() => {
currentScreen.current = 'NewScreen';
}, []);

How to dynamically show Drawer items in React-Navigation V5?

I am having a bit of trouble trying to dynamically show different items at React-Navigation drawer.
I have a drawer that needs to show different items if the user is logged in or not. In this case, if the user is logged in, the buttons for "RegisterScreen" and "LoginScreen" should NOT be shown and the "Log Out" button should be shown. However, if the user is NOT logged in, then both Register and Login screens should be shown and the Log out button should not.
I have successfully shown the buttons using the code below (Except for the Logout button, I did not do it yet). However, once the user logs out, the app redirects them to the login screen. Since I did not declare them in the drawer, I get the following error:
console.error: The action 'NAVIGATE' with payload {"name":"LoginModal"} was not handled by any navigator.
I do understand that the screens were not added, but I have no idea how they can be hidden when the user is logged in.
Code:
//Added DrawerItemList for the Logout button.
const CustomDrawerContent = (props) => {
return (
<DrawerContentScrollView {...props}>
<DrawerItemList {...props} />
<DrawerItem
label="Logout"
onPress={async () => {
props.navigation.closeDrawer();
await LogOut().then((result) => {
props.navigation.navigate('LoginModal');
});
}}
/>
</DrawerContentScrollView>
);
};
const [drawerCategories, setDrawerCategories] = useState([]);
//Checks if the user is logged in and sets Login and Register screens in the state.
useEffect(() => {
let mounted = true;
if (mounted) {
AsyncStorage.getItem('userId').then((result) => {
console.log(result);
if (result == null) {
setDrawerCategories([
...drawerCategories,
{name: 'LoginModal', component: {LoginScreen}},
{name: 'RegisterModal', component: {RegisterScreen}},
]);
}
});
}
return () => (mounted = false);
}, []);
return (
<NavigationContainer>
<RootDrawer.Navigator
drawerType="front"
initialRouteName="Home"
mode="modal"
drawerContent={(props) => <CustomDrawerContent {...props} />}>
<RootDrawer.Screen name="Home" component={MainStackScreen} />
//Add the Register and Login buttons here
{drawerCategories.map((drawer) => {
<RootDrawer.Screen name={drawer.name} component={drawer.component} />;
})}
</RootDrawer.Navigator>
</NavigationContainer>
);
Can you please help me figuring out how to fix this error? Thank you so much!
Not sure what your LogOut function does, but unless it actually re-runs the effect so that your LoginModal screen is defined, there won't be a screen named LoginModal to navigate to.
Looking at your code, your effect won't re-run after you logout. Instead of setting the list of screens in the effect, you should arrange your code to something like this:
Global store like Redux, React Context or whatever you prefer to store the userId
The useEffect hook should restore try to restore the userId from AsyncStorage like you're doing now, but after restoring, it should update your global store to update the userId there instead of setting the screens
Your logOut function should delete the userId from AsyncStorage as well as update the global store so that it's null.
Inside your component, you should conditionally define the screens based on if userId is null, and they'll automatically change on login or logout
The docs for authentication flow show an example of how this works using a token and stack navigator: https://reactnavigation.org/docs/auth-flow/
You can use similar strategy with an user id and a drawer navigator.

useEffect not called in React Native when back to screen

How are you.
This is scenario of this issue.
Let's say there are 2 screens to make it simple.
enter A screen. useEffect of A screen called.
navigate to B screen from A screen
navigate back to A screen from B.
at this time, useEffect is not called.
function CompanyComponent(props) {
const [roleID, setRoleID] = useState(props.user.SELECTED_ROLE.id)
useEffect(()=>{
// this called only once when A screen(this component) loaded,
// but when comeback to this screen, it doesn't called
setRoleID(props.user.SELECTED_ROLE.id)
}, [props.user])
}
So the updated state of Screen A remain same when comeback to A screen again (Not loading from props)
I am not changing props.user in screen B.
But I think const [roleID, setRoleID] = useState(props.user.SELECTED_ROLE.id) this line should be called at least.
I am using redux-persist. I think this is not a problem.
For navigation, I use this
// to go first screen A, screen B
function navigate(routeName, params) {
_navigator.dispatch(
NavigationActions.navigate({
routeName,
params,
})
);
}
// when come back to screen A from B
function goBack() {
_navigator.dispatch(
NavigationActions.back()
);
}
Is there any callback I can use when the screen appears?
What is wrong with my code?
Thanks
Below solution worked for me:
import React, { useEffect } from "react";
import { useIsFocused } from "#react-navigation/native";
const ExampleScreen = (props) => {
const isFocused = useIsFocused();
useEffect(() => {
console.log("called");
// Call only when screen open or when back on screen
if(isFocused){
getInitialData();
}
}, [props, isFocused]);
const getInitialData = async () => {}
return (
......
......
)
}
I've used react navigation 5+
#react-navigation/native": "5.6.1"
When you navigate from A to B, component A is not destroyed (it stays in the navigation stack). Therefore, when you navigate back the code does not run again.
Perhaps a better way to acheive what you want to to use the navigation lifecycle events (I am assuming you are using react-navigation) I.e. subscribe to the didFocus event and run whatever code you want whenever the component is focussed E.g
const unsubscribe = props.navigation.addListener('didFocus', () => {
console.log('focussed');
});
Don't forget to unsubscribe when appropriate e.g.
// sometime later perhaps when the component is unmounted call the function returned from addListener. In this case it was called unsubscribe
unsubscribe();
The current version of React Navigation provides the useFocusEffect hook. See here.
React Navigation 5 provide a useFocusEffect hook, is analogous to useEffect, the only difference is that it only runs if the screen is currently focused. Check the documentation https://reactnavigation.org/docs/use-focus-effect
useFocusEffect(
useCallback(() => {
const unsubscribe = setRoleID(props.user.SELECTED_ROLE.id)
return () => unsubscribe()
}, [props.user])
)
The above mentioned solution would work definitely but in any case you need to know why it happens here is the reason,
In react native all the screens are stacked meaning they follow the LAST-IN-FIRST-OUT order, so when you are on a SCREEN A and go.Back(), the component(Screen A) would get un-mounted because it was the last screen that was added in the stack but when we naviagte to SCREEN B, it won't get un-mounted and the next SCREEN B would be added in the stack.
So now, when you go.Back() to SCREEN A, the useEffect will not run because it never got un-mounted.
React-native keeps the navigation this way to make it look more responsive and real time.
if you want to un-mount the Screen everytime you navigate to some other screen you might want to try navigation.replace than navigation.navigate
Hope this helps you.
import { useIsFocused } from "#react-navigation/native";
const focus = useIsFocused(); // useIsFocused as shown
useEffect(() => { // whenever you are in the current screen, it will be true vice versa
if(focus == true){ // if condition required here because it will call the function even when you are not focused in the screen as well, because we passed it as a dependencies to useEffect hook
handleGetProfile();
}
}, [focus]);
more
Solution with useEffect
import { useNavigation } from '#react-navigation/native';
const Component = (props) => {
const navigation = useNavigation()
const isFocused = useMemo(() => navigation.isFocused(), [])
useEffect(() => {
if (isFocused) {
// place your logic
}
}, [isFocused])
}

React Native navigation goes back on first click, but works the second time

I have multiple dynamic buttons on the first screen, that should, when clicked, navigate to another screen with some parametars.
The problem is that after I add another button, navigation does not work the first time, instead it takes me to the targeted component, triggers willFocus navigation listener, runs componentDidUpdate(), but instead of rendering the targeted component I just get redirected to my original component which gets rerendered.
After that, if I click the same button, navigation works normally and the targeted component renders with all of the navigation params.
Here is my code:
// component with the buttons that should navigate to another component
// in render
...
<Button primary onPress={() => {
this.onNewJobPress(item.template_id);
}}>
// event handler
onNewJobPress(template_id) {
console.log( 'inside' ); // both console.logs are triggered
console.log( template_id ); // id is correct
this.props.navigation.navigate('CreateJobScreen', {
template_id: template_id,
new_template: false
});
}
// targeted component
componentWillMount() {
...
// adding focus listener
this.props.navigation.addListener('willFocus', () => {
// I can see both of these logs in the console before I'm
// redirected back to the original component
console.log('focused');
console.log(this.props.navigation.getParam('template_id'));
}
}
componentDidUpdate(prevProps, prevState) {
// I can also see this before being redirected back
console.log('did update');
}
There are no this.props.navigation.navigate methods in the targeted component that navigate back to the first component.

Resources