How to clean up state in class component in React Native? - javascript

I'm a newbie in React Native and struggling in cleaning up the state of the screen.
Like screen A has some states --> screen B, back to screen A, old states are clear. I'm using React Navigation V5
What I'm trying to do is:
After navigating from MainMap.js to the last screen TripsListScreen.js (the whole process is a Stack of 4 screens, nested in a Drawer), I got all the data stored in Redux's store, and display it in TripsListScreen.
The problem is when I press the add button in TripsListScreen to comeback at the MainMap.js, it doesn't clean up every state as I expect.
Here's the MainMap.js 's states:
const initialState = {
hasMapPermission: false,
_userLocationDisplayed: null,
userLatitude: 0,
userLongitude: 0,
initial_UserLatitude: 0,
initial_UserLongitude: 0,
userLocationAddress: '',
destination: [],
destinationName: [],
destinationAddress: [],
destinationCoords: [],
destinationImageUri: [],
numOfInput:[0,1],
counter: 1,
wayPoints: [],
markers: [],
}
class MainMap extends React.Component{
constructor(props){
super(props);
this.state = initialState;
};
componentDidMount(){
console.log('Hello',this.props)
if(this.props.route.params === true){
this.setState(initialState)
}
this._requestUserLocation();
};
Basically, I tried to pass a boolean param from TripsListScreen to MainMap, and if the param is true, I'll set all the states back to the beginning. However, it doesn't work as expected.
Here's TripsListScreen:
//...
<Layout style={styles.bottom}>
<TouchableOpacity onPress={() => props.navigation.navigate('PlanningProcess', {
screen: 'MainMapScreen',
params: {doAddPlace: true}
})} style={styles.createTrip}>
<Layout style={styles.button} >
<Icon name='add' size={35} />
</Layout>
</TouchableOpacity>
</Layout>
//...
Here's the Navigation:
StackNavigators:
const Stack = createStackNavigator();
const StackNavigator = (props) => {
return(
<Stack.Navigator screenOptions={{headerShown: false}}>
<Stack.Screen name='MainMapScreen' component={MainMap} />
<Stack.Screen name='TripDescription' component={TripDescription} />
<Stack.Screen name='TripsListDetailScreen' component={TripsListDetailScreen} />
<Stack.Screen
name='TripsListScreen'
component={TripsListScreen}
options={{
headerLeft: () => (<Icon style={{marginHorizontal: 30, marginTop: 30}} color='white' name='menu' size={30} onPress={() => props.navigation.dispatch(DrawerActions.openDrawer())}/>),
title:'Something'
}}
/>
</Stack.Navigator>
);
};
export default StackNavigator;
Main Navigators:
const Navigator = () => {
return(
<NavigationContainer>
<Drawer.Navigator
statusBarAnimation='slide'
drawerContent={props =>
<DrawerContent {...props} />}>
<Drawer.Screen name='Welcome' component={WelcomeStackScreen} />
<Drawer.Screen name='TripsListScreen' component={TripsListScreen} />
<Drawer.Screen name='PlanningProcess' component={StackNavigators} />
</Drawer.Navigator>
</NavigationContainer>
);
};
export default Navigator;
This is what MainMap renders:
This is what I expected, when navigating from TripsListScreen ( to create a new trip):
PLEASE HELP !!!

ComponentDidMount in MainMap.js doesn't get triggered because the screen is mounted already. Please look at this `componentDidMount()` function is not called after navigation

The method ComponentDidMount() only triggers for the first time at the mounting of the component and as you are navigating to a new screen, the previous screen is still mounted in the stack.
If you want to re-initialize your state every time your component gets the focus, you can set a listener on the focus on the navigation.
Like this,
const unsubscribe = navigation.addListener('willFocus', () => {
// Do something
// re-initialise the state
});

in StackNavigator, screens don't unmount when you open new screens on top of them. So if you go from A to B, then from B to C, both A and B will stay mounted. If you go back from C to B, C will unmount. It's like push and pop methods on array. componentDidMount in MainMap is not being called when you go back to it, as it doesn't unmount in first place. It is explained here Navigation Lifecycle.
As you are using Redux and saying that all the data is stored in Redux store, make your MainMap component render solely from the data from store, not from own state. You can then manipulate this data from TripsListScreen by dispatching actions. The easiest would be creating something like RESET_MAIN_MAP action that will reset that part of the state for MainMap screen

Related

Wrong back button behavior in react native

guys I have a question about navigation in react native.
So I mainly use TabNavigator. I have 2 main stack navigators in the app
const Tab = createBottomTabNavigator();
export default function App() {
return (
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeStackScreen} />
<Tab.Screen name="Profile" component={ProfileStackScreen} />
</Tab.Navigator>
</NavigationContainer>
);
}
In my ProfileStack screen, I have also two pages: MyProfile and UsersProfile:
const ProfileStack = createNativeStackNavigator();
function ProfileStackScreen({route, navigation}) {
return (
<ProfileStack.Navigator initialRouteName="MyProfile">
<ProfileStack.Screen name="MyProfile" component={MyProfilePage} />
<ProfileStack.Screen name="UserProfile" component={UserProfilePage} options={{
headerLeft: () => (<View>
<Button title="back" onPress={() => {navigation.goBack()}}/>
</View>)
}}/>
</ProfileStack.Navigator>
);
}
Now I want to navigate from the HomeScreen to the UserProfilePage and pass params to this screen. I'm doing it like this:
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Home screen this</Text>
<Button
title="Go to another user profile"
onPress={() => navigation.navigate('Profile', {screen: 'UserProfile', params: {userId: 1235}})}
/>
</View>
);
}
So now, when I come to the page UserProfile I see that it loads also the Profile page, and just for a second I see blinking of ProfilePage and its UI that is not cool, and should it be like so? I guess not.
Then if I press the BACK button on UsersProfilePage, I'm navigating back to HomeScreen - and this is ok! This is what I expect!
But now, If I will press ProfileTab I see only UsersProfilePage but not MyProfilePage. When I press the BACK button again, I go back to the HomeScreen that is weird for me. Can u explain why it happens? Why I don't get back to the MyProfilePage.
I prepared an expo snack here. You can reproduce this behavior.
This is because you’re navigating to a screen in a nested navigator. It is ignoring the initial route. Then when you press the tab again it is still mounted and will still have the previous route state, which is just the screen without the initial screen.
By default, when you navigate a screen in the nested navigator, the specified screen is used as the initial screen and the initial route prop on the navigator is ignored. This behaviour is different from the React Navigation 4.
If you need to render the initial route specified in the navigator, you can disable the behaviour of using the specified screen as the initial screen by setting initial: false:
navigation.navigate('Root', {
screen: 'Settings',
initial: false,
});
See https://reactnavigation.org/docs/nesting-navigators/#rendering-initial-route-defined-in-the-navigator and https://reactnavigation.org/docs/navigation-lifecycle/

React Native - how do you call the function of a child component from its parent (without triggering an infinite loop)?

I am trying to create a React Native e-commerce app where the featured products are shown, but then the user can view a list of categories via a sheet popping up from the bottom, which will load the products from said category.
I have managed to create such a bottom sheet using react-native-btr's BottomSheet. However, the function to show/hide the component (simply toggling a boolean in state) needs to be available to the component itself (to handle the back button and backdrop presses).
This is the component code:
const TestNav = (props, ref) => {
const [visible, setVisible] = useState(false);
const toggleVisible = () => {
setVisible(!visible);
};
useImperativeHandle(ref, () => toggleVisible());
return (
<BottomSheet
visible={visible}
//setting the visibility state of the bottom shee
onBackButtonPress={toggleVisible}
//Toggling the visibility state on the click of the back botton
onBackdropPress={toggleVisible}
//Toggling the visibility state on the clicking out side of the sheet
>
<View style={styles.bottomNavigationView}>
<View
style={{
flex: 1,
flexDirection: 'column',
}}
>
{DummyData.map((item) => {
return (
<Button
key={item.id}
title={item.name}
type="clear"
buttonStyle={styles.button}
onPress={() => console.log(item.name)}
/>
);
})}
</View>
</View>
</BottomSheet>
);
};
export default React.forwardRef(TestNav);
And here is the code for the screen where it's being used (it's called ChatScreen as I'm using it as a testing ground since I haven't implemented that feature yet)
import React, { useRef } from 'react';
import {SafeAreaView,StyleSheet,View,Text} from 'react-native';
import TestNav from '../components/TestNav';
import { Button } from 'react-native-elements';
const ChatScreen = () => {
const childRef = useRef(null);
const toggleBottomNavigationView = () => {
if (myRef.current) {
childRef.current.toggleVisible;
}
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.container}>
<Text
style={{
fontSize: 20,
marginBottom: 20,
textAlign: 'center',
}}
>
Content goes here
</Text>
<Button
onPress={() => toggleBottomNavigationView()}
title="Show Bottom Sheet"
/>
<TestNav ref={childRef} />
</View>
</SafeAreaView>
);
};
export default ChatScreen;
However, this code has somehow triggered an infinite loop, as I get this message:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
How do I go about fixing this?
I think the issue lies with how you define the imperative handle. Hook callbacks are called each time the component renders and so () => toggleVisible() is called each render and creates a render loop. It should be passed a callback that returns the imperative functions/values to be made available to callees.
const toggleVisible = () => {
setVisible(visible => !visible);
};
useImperativeHandle(ref, () => ({
toggleVisible,
}));
In ChatScreen you then need to invoke the function. I'll assume the myRef in your snippet was a typo since it's not declared in the component and the usage appears to be similar to the guard pattern.
const toggleBottomNavigationView = () => {
childRef.current && childRef.current.toggleVisible();
// or childRef.current?.toggleVisible();
};

React Navigation 5 - Reset a stack (similar to popToTop()) from another stack in a different tab before navigating to it

Suppose two stack screens in a Tab Navigator:
Tab A -> Camera
Tab B -> Profile
In the profile screen, there are other screens of the same type ("Profile") pushed (with different params) in its stack. Now, if you are in the "Camera" screen and do:
navigation.navigate("Profile", { screen: "Profile", params });
You will navigate to the "Profile" screen and those params will be sent to the last screen in the stack. What can I do if I want to navigate to the root of the stack passing the params?
I have tried with:
// In the profile screen
useEffect(() => {
if (navigation.canGoBack())
navigation.popToTop(); // Go back to the root of the stack
showParams(params);
}, [params])
but with this, the "showParams" operation isn't performed in the root, and I am not navigating directly to the root of the stack from the "Camera" screen.
I think I have to do something like this in the Camera screen before navigating:
navigation.dispatch(
CommonActions.reset({
// some stuff
})
);
navigation.navigate("Profile", { screen: "Profile", params });
But I can't find any way to achieve my goal.
Any ideas? Thank you.
UPDATE - My navigation system
STACKS (Here I define multiple stacks: "HomeStacks", "SearchStacks", "ProfileStacks" ...)
const Stack = createStackNavigator();
export function ProfileStacks() { <------ Over this stack I do .push()
return (
<Stack.Navigator
initialRouteName="Profile"
>
<Stack.Screen name="Profile" children={Profile} />
<Stack.Screen name="EditProfile" children={EditProfile} />
</Stack.Navigator>
);
}
...
BOTTOM TAB NAVIGATOR
<Tab.Navigator>
<Tab.Screen
name="Camera"
component={CameraPlaceholder}
listeners={({ navigation }) => ({
tabPress: (event) => {
event.preventDefault();
navigation.navigate("CameraModal");
},
})}
/>
<Tab.Screen
name="Profile"
component={ProfileStacks}
/>
</Tab.Navigator>
ROOT STACK NAVIGATOR (The main navigator of the app)
In this stack I implement the authentication flow and also, I declare some extra stacks (just for look-and-feel purposes).
export default function RootNavigator(props) {
/*
This navigator is implemented using the
'Protected Routes' pattern
*/
const { isUserLoggedIn } = props;
const RootStack = createStackNavigator();
return (
<RootStack.Navigator>
{isUserLoggedIn ? (
<>
<RootStack.Screen
name="BottomTabNavigator"
component={BottomTabNavigator}
/>
<RootStack.Screen
name="CameraModal"
component={Camera}
/>
</>
) : (
<>
<RootStack.Screen name="SignIn" component={SignIn} />
<RootStack.Screen
name="SignUp"
component={SignUp}
/>
<RootStack.Screen
name="ForgotPassword"
component={ForgotPassword}
/>
</>
)}
</RootStack.Navigator>
);
Related problems I have seen
How to reset a Stack in a different Tab using React Navigation 5.x
https://github.com/react-navigation/react-navigation/issues/6639
https://github.com/react-navigation/react-navigation/issues/8988
This is my Profile tab's navigation data
Object {
"key": "Profile-Ty4St1skrxoven-jkZUsx",
"name": "Profile",
"params": undefined,
"state": Object {
"index": 1,
"key": "stack-8nWDnwDJZRK8iDuJok7Hj",
"routeNames": Array [
"Profile",
"EditProfile",
],
"routes": Array [
Object {
"key": "Profile-m0GQkvNk5RjAhGABvOy9n",
"name": "Profile",
"params": undefined,
},
Object {
"key": "Profile-tZAEmSU0eEo1Nt7XC09t1",
"name": "Profile",
"params": Object {
"otherUserData": Object {
"username": "jeffbezos",
},
"post": null,
},
},
],
"stale": false,
"type": "stack",
},
},
],
"stale": false,
"type": "tab",
},
I just need to pop the second route from the stack "Profile" which is in the tab "Profile" from another Tab of my app, and then navigate to this screen.
UPDATE (Refactored code)
import { useNavigation, CommonActions } from "#react-navigation/native";
export default function useResetProfileStackNavigator() {
const navigation = useNavigation();
return () => {
const bottomTabNavigator = navigation
.getState()
?.routes?.find(({ name }) => name === "BottomTabNavigator");
const profileTab = bottomTabNavigator?.state?.routes?.find(
({ name }) => name === "ProfileStacks"
);
const { key: target, routes } = profileTab?.state ?? {};
if (!target || routes?.length <= 1) return;
routes.length = 1; // popToTop()
navigation.dispatch({
...CommonActions.reset({ routes }),
target,
});
};
}
And here is how to use it:
export default function useSendPostToProfile() {
const navigation = useNavigation();
const isSending = useRef(false);
const resetProfileStackNavigator = useResetProfileStackNavigator();
return (post) => {
if (isSending.current) return;
isSending.current = true;
// Make sure there is only one route open in the profile stack
resetProfileStackNavigator();
navigation.navigate("BottomTabNavigator", {
screen: "ProfileStacks",
params: {
screen: "Profile",
params: {
post,
},
},
});
};
}
Previous solution
After a few hours studying the problem I have found a solution. It's not the best but it works for my use case and it surely works for other people's.
What I have tried to achieve is to reset the routes of the "Profile" screen that is in a stack navigator that in turn is in another tab of the tab navigator in which my current stack screen is. It sounds somewhat confusing, but it is basically something similar to what happens on instagram when you upload a photo.
If in Instagram you navigate to other users profiles from the Home screen and then upload a photo to your account, you will see how you go from the "publish your photo" screen to the root of the stack navigator that is in the Home tab, the feed.
In my use case, I am doing something similar, I can navigate to other users profiles from my own profile, and the photos are uploaded in this screen, with a progress bar.
From the beginning I had in mind to use navigation.popToTop (), but I have not been able to obtain the result I wanted, because as I have commented previously in the question, the parameters (that contained the post) were lost. So I have no choice but to simulate this behavior from my "publish photo" screen.
The steps I have followed are as follows:
As my "publish photo" screen shares the navigation with my "profile" screen, through the tab navigator (which is obvious, since if it were not like that I could not do the navigation.navigate()), I have followed the navigation path from this to the Stack Navigator of the Profile Tab and then I have tried to take both its key and its routes.
In case I have found the current key and paths, that means the stack navigator is mounted (in my case, the tab does a lazy initialization of all my pages, that's why I speak of "trying to take"). So it will be necessary to apply steps 3 and 4.
Simulate the navigation.popToTop() reducing the size of the routes to 1 (note that the root of the stack navigator is the item in the first position of the "routes" array)
Dispatch the reset operation over the profile's stack navigator using the navigation API.
The final step, navigate to the stack screen normally passing the photo as param.
Here is the code:
const resetProfileStackNavigator = () => {
const currentNavigationState = navigation.dangerouslyGetState();
// Find the bottom navigator
for (let i = 0; i < currentNavigationState?.routes?.length; i++) {
if (currentNavigationState.routes[i].name === "BottomTabNavigator") {
// Get its state
const bottomNavigationState = currentNavigationState.routes[i].state;
// Find the profile tab
for (let j = 0; j < bottomNavigationState?.routes?.length; j++) {
if (bottomNavigationState.routes[j].name === "Profile") {
// Get its state
const profileTabState = bottomNavigationState.routes[j].state;
// Get the key of the profile tab's stack navigator
var targetKey = profileTabState?.key;
var targetCurrentRoutes = profileTabState?.routes;
break;
}
}
break;
}
}
// Reset the profile tab's stack navigator if it exists and has more than one stacked screen
if (targetKey && targetCurrentRoutes?.length > 1) {
// Set a new size for its current routes array, which is faster than Array.splice to mutate
targetCurrentRoutes.length = 1; // This simulates the navigation.popToTop()
navigation.dispatch({
...CommonActions.reset({
routes: targetCurrentRoutes, // It is necessary to copy the existing root route, with the same key, to avoid the component unmounting
}),
target: targetKey,
});
}
}
/*
Maybe, the stack navigator of the profile tab exists and has changed from its initial state...
In this situation, we will have to find the key of this stack navigator, which is also
nested in the same tab navigator in which this stack screen is.
*/
resetProfileStackNavigator();
// Finally, navigate to the profile stack screen and pass the post as param
navigation.navigate("Profile", {
screen: "Profile",
params: {
post,
},
});
Pd: I know there are some applicable refactorings, but I prefer to display the code this way so that the steps I discussed above are clearly visible.
If anyone who has read this manages to generalize this segment of code into a
generic function using ES6, please leave it as an answer, as it can be very useful for me and for other users.
I was struggling with similar issue. My case was that I want to have the same stack navigator and the tabs would be just different starting points, something like 2 home screens. It is the behavior that is seen in Spotify for Android, for example - we have Home, Search and Library and all of them have common screens such as Album Screen and Song Screen. And when the user clicks on one of the tabs, the stack is cleared (like popToTop() should do).
My solution was to have Bottom Tab Navigator with Stack Navigators that have the same screens - DiscoverNavigator and SearchNavigator:
const SearchNavigator = () => (
<Stack.Navigator headerMode='screen'>
<Stack.Screen name='Search' component={SearchScreen} />
<Stack.Screen name='SearchResults' component={SearchResultsScreen} />
<Stack.Screen name='Item' component={ItemScreen} />
</Stack.Navigator>
)
const DiscoverNavigator = () => (
<Stack.Navigator headerMode='screen'>
<Stack.Screen name='Discover' component={DiscoverScreen} />
<Stack.Screen name='Search' component={SearchScreen} />
<Stack.Screen name='SearchResults' component={SearchResultsScreen} />
<Stack.Screen name='Item' component={ItemScreen} />
</Stack.Navigator>
)
And the trick is to add a listener on blur for the Tabs, like that:
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name='DiscoverNavigator' component={DiscoverNavigator}
listeners={props => tabPressListener({ ...props })}
/>
<Tab.Screen name='SearchNavigator' component={SearchNavigator}
listeners={props => tabPressListener({ ...props })}
/>
</Tab.Navigator>
</NavigationContainer>
That the handler for the blur event will check if the current tab has its own stack navigation and if it should clear it:
const tabPressListener = props => {
const { navigation } = props
return {
blur: e => {
const target = e.target
const state = navigation.dangerouslyGetState()
const route = state.routes.find(r => r.key === target)
// If we are leaving a tab that has its own stack navigation, then clear it
if (route.state?.type === "stack" && route.state.routes?.length > 1) {
navigation.dispatch(StackActions.popToTop())
}
}
}
}
Here is a demo: https://snack.expo.io/#monikamateeva/bottom-tab-navigation-with-poptotop
And this is all of the code:
import { createBottomTabNavigator } from '#react-navigation/bottom-tabs'
import { NavigationContainer, CommonActions, StackActions } from '#react-navigation/native'
import { createStackNavigator } from '#react-navigation/stack'
import React from 'react'
import { Button, StyleSheet, View } from 'react-native'
import { enableScreens } from 'react-native-screens';
enableScreens();
const Stack = createStackNavigator()
const Tab = createBottomTabNavigator()
const ItemScreen = ({ navigation, route }) => {
React.useLayoutEffect(() => {
navigation.setOptions({
title: `Item ${route.params?.id}`,
})
}, [navigation, route])
return (
<View style={styles.container}>
<Button title='Item 2' onPress={() => navigation.push('Item', { id: 2 })} />
</View>
)
}
const SearchResultsScreen = ({ navigation, route }) => (
<View style={styles.container}>
<Button title={`Item ${route.params?.id}`} onPress={() => navigation.push('Item', { id: route.params?.id })} />
</View>
)
const DiscoverScreen = ({ navigation }) => (
<View style={styles.container}>
<Button title='Search Results 20' onPress={() => navigation.navigate('Search', { screen: 'SearchResults', params: { id: 20 } })} />
<Button title='Item 20' onPress={() => navigation.navigate('Search', { screen: 'Item', params: { id: 20 } })} />
</View>
)
const SearchScreen = ({ navigation }) => (
<View style={styles.container}>
<Button title='Search Results 10' onPress={() => navigation.push('SearchResults', { id: 20 })} />
<Button title='Item 10' onPress={() => navigation.push('Item', { id: 10 })} />
</View>
)
const SearchNavigator = () => (
<Stack.Navigator headerMode='screen'>
<Stack.Screen name='Search' component={SearchScreen} />
<Stack.Screen name='SearchResults' component={SearchResultsScreen} />
<Stack.Screen name='Item' component={ItemScreen} />
</Stack.Navigator>
)
const DiscoverNavigator = () => (
<Stack.Navigator headerMode='screen'>
<Stack.Screen name='Discover' component={DiscoverScreen} />
<Stack.Screen name='Search' component={SearchScreen} />
<Stack.Screen name='SearchResults' component={SearchResultsScreen} />
<Stack.Screen name='Item' component={ItemScreen} />
</Stack.Navigator>
)
const tabPressListener = props => {
const { navigation } = props
return {
blur: e => {
const target = e.target
const state = navigation.dangerouslyGetState()
const route = state.routes.find(r => r.key === target)
// If we are leaving a tab that has its own stack navigation, then clear it
if (route.state?.type === "stack" && route.state.routes?.length > 1) {
navigation.dispatch(StackActions.popToTop())
}
},
// Log the state for debug only
state: e => {
const state = navigation.dangerouslyGetState()
console.log(`state`, state)
}
}
}
const AppNavigator = () => {
return (
<NavigationContainer name="BottomTabNavigator">
<Tab.Navigator>
<Tab.Screen
name='DiscoverNavigator'
component={DiscoverNavigator}
listeners={props => tabPressListener({ ...props })}
/>
<Tab.Screen
name='SearchNavigator'
component={SearchNavigator}
listeners={props => tabPressListener({ ...props })}
/>
</Tab.Navigator>
</NavigationContainer>
)
}
export default AppNavigator
const styles = StyleSheet.create({
container: {
flex: 1,
},
})
This code is enough and it works
<Tab.Screen name='SearchNavigator' component={SearchNavigator}
listeners={props => tabPressListener({ ...props })}/>
and this
const tabPressListener = props => {
const { navigation } = props
return {
blur: e => {
const target = e.target
const state = navigation.dangerouslyGetState()
const route = state.routes.find(r => r.key === target)
// If we are leaving a tab that has its own stack navigation, then clear it
if (route.state?.type === "stack" && route.state.routes?.length > 1) {
navigation.dispatch(StackActions.popToTop())
}
}
}
}
As of React Navigation 6.x you can easily achieve this with CommonActions.reset. I wrote some example code based on the original question assuming only one stack, haven't tested for nested stacks but the solution might be something similar.
import {CommonActions} from '#react-navigation/native';
navigation.dispatch((state) => {
const params = state.routes[state.routes.length - 1].params;
return CommonActions.reset({
index: 1,
routes: [{name: 'Home', params}]
});
});

How to render React Route component in an entirely new, blank page

I'm trying to render a print page using React Router. So I have two components:
export default class PurchaseOrder extends React.Component{
....
render(){
const {orderDate, client} = this.state.order;
//omitted for brevity
return(
<BrowserRoute>
<Button
component={Link}
to="/order/print"
target="_blank"
>
Print
</Button>
<Route
path="/order/print"
render={props => (
<OrderPrint
{...props}
orderDate={orderDate}
client={client}
/>
)}
/>
</BrowserRoute>
}
}
And the OrderPrint:
export default function OrderPrint(props) {
return (
<div>props.orderDate</div>
<div>props.client.name</div>
);
}
As you can see, I'm trying to present the printable version of the purchase order with a click of a button. The OrderPrint component gets rendered, but it's rendered right below the button. I could put the Route inside my root component, which is App, that way making sure that I get only the contents of the OrderPrint component rendered like this:
class App extends Component {
render() {
return (
<div className="App">
<Router>
<Route exact path="/" component={PurchaseOrder} />
<Route exact path="/order/print" component={OrderPrint} />
</Router>
</div>
);
}
}
But in that case, I won't be able to pass the necessary props to it. So in this particular case, how to replace entire page content with the contents of OrderPrint component and still be able to pass the necessary input to it?
Update
As #Akalanka Weerasooriya mentioned in comments, I could have the entire state kept in the App component. But one thing stopped me from doing this: This means I'll practically always have to use the render prop of the Route component, instead of the component prop. Ok, that's not a problem, but if it's the way to go, then why does React Router documentation almost always use the
<Route path="/about" component={About} />
pattern as the standard way of using it? So to recap it, if I go the Single Source of Truth way and store all my state in one place, then doesn't it mean that I will always use
<Route path="/about" render={props=>(<div>props.someProp</div>)} />
I don't say there's a problem with it, it's just mentioning it in the documentation only after component={SomeComponent} pattern confuses me.
Not sure why you need a different route for a print page, but anyway if you want it on a new empty page, you can take advantage of the ReactDOM.createPortal feature.
You can create a new page and or even a new window using window.open while keeping the flow of react data in sync.
Here is a running example of a portal on a new window with live state updates from the component that triggered this window using a portal:
running example, i'm sharing an external snippet and not using stack-snippets here because window.open returns null in the contexts of stack-snippets
Source code:
class WindowPortal extends React.PureComponent {
containerEl = document.createElement("div");
externalWindow = null;
componentDidMount() {
const { width = 450, height = 250, left = 150, top = 150 } = this.props;
const windowFetures = `width=${width},height=${height},left=${left},top=${top}`;
this.externalWindow = window.open("", "", windowFetures);
this.externalWindow.document.body.appendChild(this.containerEl);
}
componentWillUnmount() {
this.externalWindow.close();
}
render() {
return ReactDOM.createPortal(this.props.children, this.containerEl);
}
}
class App extends React.PureComponent {
state = {
counter: 0,
showWindowPortal: false
};
componentDidMount() {
window.setInterval(() => {
this.setState(state => ({
counter: state.counter + 1
}));
}, 1000);
}
toggleWindowPortal = () => {
this.setState(state => ({
...state,
showWindowPortal: !state.showWindowPortal
}));
};
closeWindowPortal = () => {
this.setState({ showWindowPortal: false });
};
render() {
return (
<div>
<h1>Counter: {this.state.counter}</h1>
<button onClick={this.toggleWindowPortal}>
{this.state.showWindowPortal ? "Close the" : "Open a"} Portal
</button>
{this.state.showWindowPortal && (
<WindowPortal closeWindowPortal={this.closeWindowPortal}>
<h2>We are in a portal on a new window</h2>
<h3>{`This is the current state: ${this.state.counter}`}</h3>
<p>different window but sharing the state!!</p>
<button onClick={() => this.closeWindowPortal()}>Close me!</button>
</WindowPortal>
)}
</div>
);
}
}
here you have a PrivateRoute which is a custom route which holds a header and header is rendered in PrivateRoute routes only so when you try to navigate to new route like path="/order/print" then you won't get header which has button in it.
function Header(props) {
return (
<div>
<Button
component={Link}
to="/order/print"
target="_blank">
Print</Button>
{props.children}
</div>
)
}
const PrivateRoute = ({ component: Component, layout: Layout, ...rest }) => {
return <Route {...rest} render={props => {
return <Layout>
<Component {...props} />
</Layout>
}} />
}
export default class PurchaseOrder extends React.Component{
render(){
const {orderDate, client} = this.state.order;
//omitted for brevity
return(
<BrowserRouter>
<div>
<PrivateRoute exact path="/" layout={Header} component={Landing} />
<Route
path="/order/print"
render={props => (
<OrderPrint
{...props}
orderDate={orderDate}
client={client}
/>
)}
/>
</div>
</BrowserRouter>
}
}

React Native: setState + unmounted or mounting component

I am trying to conditionally display either a Home or Slider component in the screen below, but when the onDone function runs, i am getting the error:
Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
Please check the code for the Onboarding component.
The Onboarding component is inside the Slider (react-native-onboarding-swiper - used for app intro)...
export default class HomeScreen extends Component {
static navigationOptions = {
headerStyle: {
backgroundColor: 'skyblue',
elevation: 0,
borderBottomWidth: 0,
},
headerLeft: null,
};
state = {
introLoaded: false,
};
async componentDidMount() {
const value = await AsyncStorage.getItem('#SKIP_INTRO');
if (value !== null) {
this.onDone();
}
};
onDone = async () => {
await this.setState({ introLoaded: true });
};
render() {
return this.state.introLoaded ? (
<Home navigation={this.props.navigation} />
) : (
<Slider onDone={this.onDone} />
);
}
}
Any help appreciated...
Slider.js
import React from 'react';
import { Image, Text } from 'react-native';
import PropTypes from 'prop-types';
import Onboarding from 'react-native-onboarding-swiper';
import styles from './styles';
const Slider = ({ onDone }) => (
<Onboarding
pages={[
{
backgroundColor: 'skyblue',
image: (
<Image source={require('../../assets/images/intro/pic1.png')} style={styles.image} />
),
title: <Text style={styles.title}>Title 1</Text>,
subtitle: <Text style={styles.subtitle}>Subtitle 1</Text>,
},
{
backgroundColor: 'skyblue',
image: (
<Image source={require('../../assets/images/intro/pic2.png')} style={styles.image} />
),
title: <Text style={styles.title}>Title 2</Text>,
subtitle: <Text style={styles.subtitle}>Subtitle 2</Text>,
},
]}
onDone={onDone}
/>
);
Slider.propTypes = {
onDone: PropTypes.func.isRequired,
};
export default Slider;
First setState is not an asynchronous method. For more information read here.
Second in your approach HomeScreen is calling method onDone inside componentDidMount lifecycle method as the component mounted it will automatically unload Slider and just show error as you are changing state.
So, instead of using Onboarding inside stateless component use it inside state component and use it in the Welcome Screen (the screen where user is not logged in and see for first time). Once user logged in just navigate to the other screen so this welcome screen will not be visible to user again.
let me know if you need more information.

Categories