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';
}, []);
Related
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.
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
data should update when useEffect runs, and it does. However, the change doesn't cause a rerender in the Liked page of the Tab Navigator, where it is passed as a route.params parameter.
Why doesn't the re-render happen?
Below is the parent component Home.js, that has a Tab.Navigator as a child. I pass in the data of note data into the videoData initial param of the tab navigator. However, when I update data, videoData never updates as well.
Home.js
const [data, setData] = useState([{name: 'test'}]);
console.log('DATA', data);
// {lastVisible} for keep track of firebase paging
const Tab = createMaterialTopTabNavigator();
const navigator =
<Tab.Navigator>
<Tab.Screen name="posts" component={FeedList} initialParams={{videoData: route.params.videoData}} />
<Tab.Screen name="Liked" component={FeedList} initialParams={{videoData: data, test: 'helloworld'}} />
</Tab.Navigator>;
useEffect(()=>{
fetchChatGroups().then((ar) => {
setData(ar);
});
},[]);
...
return <>{navigator}</>
Is initialParams not subject to re-renders of useState variables of parent components? When I print the value of route.params.videoData in FeedList, I get the following:
[Sat Jan 02 2021 19:54:41.145] LOG [{"name":"test"}]
which means the state stayed as default, even though I confirmed that the state does change with a console print with this line: console.log('DATA', data);
Try to move this line of code const Tab = createMaterialTopTabNavigator(); outside the component or wrap with useMemo.
My answer is for Tab.Screen re-render when the state value changes, I think this might help you.
Check this stack flow for more information:
[blog]: React native: React navigation tab screen does not update if children is not an inline function
So, in order to re-render the component we need to pass props to component as children.
Example:
<Tab.Screen name="posts" component={FeedList} />
Here, instead of using component={FeedList}, you can use children =>
<Tab.Screen name="posts" children={()=>this.FeedList(Pass Props Here)} />
The above example is for class component.
Other examples:
// Inside render()
<Tab.Screen
name="Account"
//component={this.AccountScreen}
children={() =>
this.AccountScreen(this.props.schoolInformationPending)
}
options={{
tabBarLabel: 'Account',
}}
/>
// Inside Class Component
AccountScreen = (props: any) => {
return (<View><Text>{props.value}</Text></View>)
}
I would like to navigate the same way you do with react-router.
I am fetching an API and display the results as cards, when you click a card, I need it to navigate to a specific ID based on the card selected.
<Route path='/item:id' component={item-details} />
and then on the card, you do this.<Link to='item/:id' > Card...</Link>
That's how you do it with react-router-dom...I need to archive the same with React Native.
Any solutions?
I suggest using react-navigation, it's the best solution for navigation for a RN app. It's not quite like React Router but RN requires a different method. In react-navigation, you declare your screens as 'navigators', e.g stack navigator.
import { createStackNavigator } from '#react-navigation/stack';
const Stack = createStackNavigator();
function MyStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Screen1" component={Screen1Component} />
<Stack.Screen name="Screen2" component={Screen2Component} />
</Stack.Navigator>
);
}
On each screen, a navigation prop is passed down. This has a lot of properties and functions but one is a function called navigate, which takes you to another screen in your stack. For example:
const Screen1Component = ({ navigation }) => (
<View>
// content
<TouchableOpacity onPress={() => navigation.navigate('Screen2) })>Go to Screen2</TouchableOpacity>
</View>
)
navigate also has a second parameter it can take called screen params. These are parameters you can pass between screens when you navigate. It's used like navigation.navigate('Screen2, { id: '1234' }).
In your Screen2 component, there will be a route prop, where you can then pick that param up and use it, for example:
const Screen2Component = ({ route, navigation }) => {
const { id } = route.params
// do something with it, call api, pass to redux action,etc
return ...
}
Hope that helps.
I am using the tabnavigator from react navigation. I have a parent component that looks like this
changeService(serviceName){
this.setState({
});
}
render() {
const { container } = styles
return (
<View style={container}>
<Header
clientName = {this.state.project}
serviceName = {this.state.service}
cancelClick = {this.handleExitClick}
/>
<ServicesNavigator
changeService = {this.changeService}
/>
</View>
);
}
Then my service navigator export looks like this
export default (props) => {
const { screenProps, ...otherProps } = props;
const ServicesNavigator = makeServiceNavigator(props);
return <ServicesNavigator screenProps={{ ...screenProps, ...otherProps
}} />
};
The navigator itself looks like a normal navigator except it hsa navigationOptions equal to
tabBarOnPress: (scene, jumpToIndex) => {
changeService(scene.scene.route.routeName)
scene.jumpToIndex(scene.scene.index)
},
Whenever i try to change tabs the navigator is reloading completely and going back to default tab selection. Is there no way to allow the tabnavigors parents state variables to change without reseting the tab navigator?
Whenever tabBarOnPress is triggered, it requests to update state of the parent component. Then due to React component lifecycle, state update will cause parent component to re-render, along with all its children. This is why your tab navigator is reseted all the times.
There are several solutions for your case:
1 - Use shouldComponentUpdate to guide when parent component to be updated. Since you want to retain ServicesNavigator whenever changeService is called, you want to avoid re-rendering on related state items.
shouldComponentUpdate(nextProps, nextState) {
if (this.state.itemA !== nextState.itemA) {
return false; // avoid re-rendering
}
return true;
}
This is dirty but working; however it will haunt you in the long run since it causes parent component's behaviours not idempotent.
Edit: I forgot you have the Header which reads the state. If you use shouldComponentUpdate in the parent component, then Header will not receive new props. So 1st option is not correct. Sorry about this.
2 - Don't let parent component hold the state anymore. In your sample code, parent component has state only because its children components need to communicate and share data with each others. So why don't you just keep the sharing data somewhere else? Best tools can be state management libraries like mobx or redux, which can be integrated quite easily with react-navigation