Current behavior
In my team we would like to upgrade react navigation to V5. Since our codebase is very large we have to do an incremental refactor (Upgrading everything in one shot is impossible).
For this reason we would like to use the helper #react-navigation/compat with the latest version ^5.3.20. So far this is the different piece of code we have updated:
Main app navigation:
At start we were using createSwitchNavigator. At startup we check if user is logged in:
If yes: redirect to MainNavigation navigator.
if no: redirect to Authentification navigator.
<NavigationContainer>
<Main.Navigator>
<Main.Screen
name={SceneKeys.Startup}
component={Startup}
options={{ header: () => null }}
></Main.Screen>
{ !authenticated ? (
<Main.Screen
name={SceneKeys.Authentication}
component={AuthenticationNavigation}
options={{ header: () => null }}
></Main.Screen>
) : (
<Main.Screen
name={SceneKeys.App}
component={MainNavigation}
options={{ header: () => null }}
></Main.Screen>
)}
</Main.Navigator>
</NavigationContainer>
This part work well, if user is not logged in we are correctly redirected to AuthenticationNavigation. The problem is in our main navigation.
const MainNavigation = createCompatNavigatorFactory(createStackNavigator)(
{
// Main stack contain many navigator (switch, drawer, etc...)
MainStack: { screen: TabBarNavigator, navigationOptions: { header: null } },
// ...other routes
}
When we redirect on this navigator we have some errors coming from withNavigation (in compat mode) and navigationOptions.
withNavigation:
The error say that we cannot use withNavigation since it's a hook and we can only call this in the body of a function. However we use the compat' mode and on the doc it says that is exported as a HOC.
export default withNavigation(
connect(mapStateToProps, mapDispatchToProps)(OurComponent),
);
Header:
Header props fail even with the compat' mode with navigationOptions and createCompatNavigatorFactory
const MessagingNavigator = createCompatNavigatorFactory(createStackNavigator)(
{
[SceneKeys.messaging]: {
screen: Messaging,
navigationOptions: ({ navigation }) => ({
header: (
<MessagingNavBar mode={'conversationsList'} navigation={navigation} />
),
drawerLockMode: navigation.state.params
? navigation.state.params.drawerLockMode
: undefined,
}),
},
If I delete navigationOptions property and I refactor withNavigation by using useNavigation() hook it works well and go to the correct screen otherwise it doesn't work.
At the moment we cannot afford to refactor everything from the ground up and we would like to do that incrementally. With the informations above, did we miss something ?
Expected behavior
Is withNavigation should work as an HOC's with component and redux-connect since it's exported from compat' plugin ?
Why navigationOptions not working with the previous headers implementation in compatibility mode?
Related
I need to define route types in the stack (not in the component), I know you can do it with RouteProp in components itself but I just couldn't figure it out how to apply that on a stack.
<Stack.Screen
name="MarketAssetDetailsPage"
options={({ route }) =>
HeaderWithBackTitle({
title: `${route.params.routeName} Market Details`,
bgColor: route.params.data.assetColor,
textColor: 'white',
})
}
component={MarketAssetDetailsPage}
/>
Here I have a screen receiving routeName and a data. Eslint and typescript complains here, how to define types here? Thanks.
You should create a type for the props.
export type StackParamList = {
MarketAssetDetailsPage: {
routeName: string,
data: any // Change to be the type of data.
}
}
Inside of your component you can do something like this
import { StackNavigationProp } from '#react-navigation/stack';
type NavigationProps = StackNavigationProp<StackParamList, 'MarketAssetDetailsPage'>;
interface Props {
navigation: NavigationProps
}
const MarketAssetDetailsPage = ({ navigation }: Props) => {
}
There is a good guide on the React Navigation website for TypeScript support - https://reactnavigation.org/docs/typescript/
I'm building a React Native app. I have imported createStackNavigator from react-navigation. I'm able to get it working on my Home screen - I click a button, it brings me to a new component. This is the code that I'm using to bring it into my Home.js
// src/components/Home/Home
export class Home extends Component {
render() {
return (
<React.Fragment>
<Button
title="Test button"
onPress={() => this.props.navigation.navigate('Roads')}
/>
<StatusBar />
<Header />
<Menu />
</React.Fragment>
);
}
}
const RootStack = createStackNavigator(
{
Home: Home,
Roads: Roads,
},
{
initialRouteName: 'Home',
}
);
export default class App extends React.Component {
render() {
return <RootStack />;
}
}
My Home page takes in a Menu which has a list of MenuItems. I am trying to get the MenuItems to jump to the appropriate pages. When I try to bring in the navigation inside MenuItem.js's render method, like so:
// src/components/Roads/Roads
render() {
const { navigate } = this.props.navigation;
console.log(this.props, "props is here");
I get the following error message:
TypeError: undefined is not an object (evaluating 'this.props.navigation.navigate').
Do I need to pass the navigator down in props to Menu.js and then to MenuItem.js? The docs give examples but it seems to be examples that assume you jam all your code into one file rather than across several components.
Have I set this up correctly?
When using a Navigator from react-navigation only the components you declare as Screens inherit the navigation prop (in your case Home and Roads)
This means that you will need to pass it as a prop to its children as you said:
<Menu navigation={this.props.navigation} />
<MenuItem navigation={this.props.navigation} />
In case anyone is wondering how to navigate from a component that isn't inside a Navigator then I suggest reading this part of the react-navigation documentation
https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html
I am having problem figuring out why my application is doing endless render.
Inside, My stateful component, I am calling a redux action in componentDidMount method (calling componentWillMount also do endless render)
class cryptoTicker extends PureComponent {
componentDidMount() {
this.props.fetchCoin()
// This fetches some 1600 crypto coins data,Redux action link for the same in end
}
render() {
return (
<ScrollView>
<Header />
<View>
<FlatList
data={this.state.searchCoin ? this.displaySearchCrypto : this.props.cryptoLoaded}
style={{ flex: 1 }}
extraData={[this.displaySearchCrypto, this.props.cryptoLoaded]}
keyExtractor={item => item.short}
initialNumToRender={50}
windowSize={21}
removeClippedSubviews={true}
renderItem={({ item, index }) => (
<CoinCard
key={item["short"]}
/>
)}
/>
</View>
</ScrollView>
)
}
}
In CoinCard I am literally doing nothing besides this (Notice CoinCard inside Flat list)
class CoinCard extends Component {
render () {
console.log("Inside rende here")
return (
<View> <Text> Text </Text> </View>
)
}
}
Now, When I console log in my coincard render, I can see infinite log of Inside rende here
[Question:] Can anyone please help me figure out why this could be happening?
You can click here to see my actions and click here to see my reducer.
[Update:] My repository is here if you want to clone and see it by yourself.
[Update: 2]: I have pushed the above shared code on github and it will still log endless console.log statements (if you can clone, run and move back to this commit ).
[Update:3]: I am no longer using <ScrollView /> in <FlatList /> also when I mean endless render, I mean is that it is endless (& Unecessarily) passing same props to child component (<Coincard />), if I use PureComponent, it won't log endlessly in render () { but in componentWillRecieveProps, If I do console.log(nextProps), I can see the same log passed over and over again
There are some points to note in your code.
The CoinCard Component must be a PureComponent, which will not re-render if the props are shallow-equal.
You should not render your Flatlist inside the ScrollView component, which would make the component render all components inside it at once which may cause more looping between the Flatlist and ScrollView.
You can also a definite height to the rendered component to reduce the number of times component is rendered for other props.
Another thing to note is, only props in the component are rendered on scroll bottom, based on the log statement mentioned below.
import {Dimensions} from 'react-native'
const {width, height} = Dimensions.get('window)
class CoinCard extends React.PureComponent {
render () {
console.log(this.props.item.long) //... Check the prop changes here, pass the item prop in parent Flatlist. This logs component prop changes which will show that same items are not being re-rendered but new items are being called.
return (
<View style={{height / 10, width}}> //... Render 10 items on the screen
<Text>
Text
</Text>
</View>
)
}
}
UPDATE
This extra logging is due to the props being from the Flatlist to your component without PureComponent shallow comparison.
Note that componentWillReceiveProps() is deprecated and you should avoid them in your code.
React.PureComponent works under the hood and uses shouldComponentUpdate to use shallow comparison between the current and updated props. Therefore log console.log(this.props.item.long) in your PureComponent' render will log the unique list which can be checked.
Like izb mentions, the root cause of the pb is the business call that is done on a pure component whereas it is just loaded. It is because your component make a business decision (<=>"I decide when something must be showed in myself"). It is not a good practice in React, even less when you use redux. The component must be as stupid a possible and not even decide what to do and when to do it.
As I see in your project, you don't deal correctly with component and container concept. You should not have any logic in your container, as it should simply be a wrapper of a stupid pure component. Like this:
import { connect, Dispatch } from "react-redux";
import { push, RouterAction, RouterState } from "react-router-redux";
import ApplicationBarComponent from "../components/ApplicationBar";
export function mapStateToProps({ routing }: { routing: RouterState }) {
return routing;
}
export function mapDispatchToProps(dispatch: Dispatch<RouterAction>) {
return {
navigate: (payload: string) => dispatch(push(payload)),
};
}
const tmp = connect(mapStateToProps, mapDispatchToProps);
export default tmp(ApplicationBarComponent);
and the matching component:
import AppBar from '#material-ui/core/AppBar';
import IconButton from '#material-ui/core/IconButton';
import Menu from '#material-ui/core/Menu';
import MenuItem from '#material-ui/core/MenuItem';
import { StyleRules, Theme, withStyles, WithStyles } from '#material-ui/core/styles';
import Tab from '#material-ui/core/Tab';
import Tabs from '#material-ui/core/Tabs';
import Toolbar from '#material-ui/core/Toolbar';
import Typography from '#material-ui/core/Typography';
import AccountCircle from '#material-ui/icons/AccountCircle';
import MenuIcon from '#material-ui/icons/Menu';
import autobind from "autobind-decorator";
import * as React from "react";
import { push, RouterState } from "react-router-redux";
const styles = (theme: Theme): StyleRules => ({
flex: {
flex: 1
},
menuButton: {
marginLeft: -12,
marginRight: 20,
},
root: {
backgroundColor: theme.palette.background.paper,
flexGrow: 1
},
});
export interface IProps extends RouterState, WithStyles {
navigate: typeof push;
}
#autobind
class ApplicationBar extends React.PureComponent<IProps, { anchorEl: HTMLInputElement | undefined }> {
constructor(props: any) {
super(props);
this.state = { anchorEl: undefined };
}
public render() {
const auth = true;
const { classes } = this.props;
const menuOpened = !!this.state.anchorEl;
return (
<div className={classes.root}>
<AppBar position="fixed" color="primary">
<Toolbar>
<IconButton className={classes.menuButton} color="inherit" aria-label="Menu">
<MenuIcon />
</IconButton>
<Typography variant="title" color="inherit" className={classes.flex}>
Title
</Typography>
<Tabs value={this.getPathName()} onChange={this.handleNavigate} >
{/* <Tabs value="/"> */}
<Tab label="Counter 1" value="/counter1" />
<Tab label="Counter 2" value="/counter2" />
<Tab label="Register" value="/register" />
<Tab label="Forecast" value="/forecast" />
</Tabs>
{auth && (
<div>
<IconButton
aria-owns={menuOpened ? 'menu-appbar' : undefined}
aria-haspopup="true"
onClick={this.handleMenu}
color="inherit"
>
<AccountCircle />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={this.state.anchorEl}
anchorOrigin={{
horizontal: 'right',
vertical: 'top',
}}
transformOrigin={{
horizontal: 'right',
vertical: 'top',
}}
open={menuOpened}
onClose={this.handleClose}
>
<MenuItem onClick={this.handleClose}>Profile</MenuItem>
<MenuItem onClick={this.handleClose}>My account</MenuItem>
</Menu>
</div>
)}
</Toolbar>
</AppBar>
</div >
);
}
private getPathName(): string {
if (!this.props.location) {
return "/counter1";
}
return (this.props.location as { pathname: string }).pathname;
}
private handleNavigate(event: React.ChangeEvent<{}>, value: any) {
this.props.navigate(value as string);
}
private handleMenu(event: React.MouseEvent<HTMLInputElement>) {
this.setState({ anchorEl: event.currentTarget });
}
private handleClose() {
this.setState({ anchorEl: undefined });
}
}
export default withStyles(styles)(ApplicationBar);
Then you will tell me: "but where do I initiate the call that will fill my list?"
Well I see here that you use redux-thunk (I prefer redux observable... more complicated to learn but waaaaaaaaaaaaaaaaay more powerful), then this should be thunk that initiates the dispatch of this!
To summarize:
Components: the stupidest element that normally should have only the render method, and some other method handler to bubble up user events. This method only takes care of showing its properties to the user. Don't use the state unless you have a visual information that belongs only to this component (like the visibility of a popup for example). Anything that is showed or updated comes from above: a higher level component, or a container. It doesn't decide to update its own values. At best, it handles a user event on a subcomponent, then bubble up another event above, and... well maybe at some point, some new properties will be given back by its container!
Container: very stupid logic that consists in wrapping a top level component into redux for it to plug events to actions, and to plug some part of the store to properties
Redux thunk (or redux observable): it is the one that handles the whole user application logic. This guy is the only one who knows what to trigger and when. If a part of your front end must contain the complexity, it's this one!
Reducers: define how to organize the data in the store for it to be as easily usable as possible.
The store: ideally one per top level container, the only one that contains the data that must be showed to the user. Nobody else should.
If you follow these principles, you should never face any issue like "why the hell this is called twice? and... who made it? and why at this moment?"
Something else: if you use redux, use an immutability framework. Otherwise you may face issues as reducers must be pure functions. For this you can use a popular one immutable.js but not convenient at all. And the late ousider that is actually a killer: immer (made by the author or mobx).
It seems Jacob in the above comment has managed to make the component render only twice.
This will definitely cause double initial render (and would cause an infinite render if it wasn't a PureComponent):
componentDidUpdate() {
var updateCoinData;
if (!updateCoinData) { // <- this is always true
updateCoinData = [...this.props.cryptoLoaded];
this.setState({updateCoinData: true}); // <- this will trigger a re render since `this.state.updateCoinData` is not initially true
}
...
}
Link to the issue in your repository
I'm using https://reactnavigation.org/ for navigation in a React Native app with a tab navigator as the main stack and a modal with two screens in it (for logging in and configuring the app).
I can't for the life of me figure out how to close the modal from the second screen (SelectItems). From the first screen in the modal I can close it with navigation.goBack().
Both modal screens need a close button. Is there a way to just return back to whatever tab the user was on?
Thanks in advance for any help.
const Tabs = TabNavigator(
{
Search: { screen: Search },
Settings: { screen: Settings }
}
);
// modal with two screens
const Setup = StackNavigator(
{
Login: {
screen: Login
},
SelectItems: {
screen: SelectItems
}
},
{
initialRouteName: 'Login'
}
);
const RootStack = StackNavigator(
{
Main: {
screen: Tabs
},
Setup: {
screen: Setup
}
},
{
mode: 'modal',
headerMode: 'none'
}
);
I found a solution but it isn't perfect.
You can use the popToTop which will go back to the first Scene of your stack and than the goBack will close the modal.
navigation.popToTop();
navigation.goBack(null);
The problem with that is that it will mount again the first scene of the stack, so be sure you dont use setState in you willMount or didMount. Or prevent it.
That's the solution i'm going with for now. I keep looking for a better solution.
Simple and easy solution for react-navigation 5.x (getParent docs):
navigation.getParent()?.goBack();
This works because it grabs the navigator's parent, which is the modal and what you want to dismiss.
NOTE: In older versions of 5.x this was called dangerouslyGetParent. That exists in newer 5.x versions, but is now deprecated. Use that if getParent isn't available in the version of react-navigation that you're using. It isn't actually dangerous: From react-navigation's documentation:
Reason why the function is called dangerouslyGetParent is to warn developers against overusing it to eg. get parent of parent and other hard-to-follow patterns.
This was my solution with v6 in 2022. It closes the modal and navigates away without any weird behaviors (at least in my case).
onPress = () => {
navigation.goBack(); // <-- this fixed it
navigation.navigate("SomeScreen", { id: 123});
}
If you use react-navigation 4.x there is a method navigation.dismiss(). The method dismisses the entire stack and return to the parent stack
https://reactnavigation.org/docs/4.x/navigation-prop/#dismiss
If you are using Stack Navigation you can always move around in the navigation stack using navigation.pop(). For instance, if you want to close two open modals you can call the pop function with parameter value 2:
navigation.pop(2);
Original solution from https://github.com/react-navigation/react-navigation/issues/686#issuecomment-342766039, updated for React Navigation 4:
Create a DismissableStackNavigator:
import React from 'react';
import { createStackNavigator } from 'react-navigation-stack';
export default function DismissableStackNavigator(routes, options) {
const StackNav = createStackNavigator(routes, options);
const DismissableStackNav = ({navigation, screenProps}) => {
const { state, goBack } = navigation;
const props = {
...screenProps,
dismiss: () => goBack(state.key),
};
return (
<StackNav
screenProps={props}
navigation={navigation}
/>
);
}
DismissableStackNav.router = StackNav.router;
return DismissableStackNav;
};
Usage:
Creating your stack:
// modal with two screens
const Setup = StackNavigator(
{
Login: Login,
SelectItems: SelectItems
},
{
initialRouteName: 'Login'
headerMode: 'none'
}
);
Call navigation.dismiss in your screens to close the modal stack.
I was trying to figure this myself and the solution I ended up using was to use navigation.navigate()
Example this.props.navigation.navigate('name of screen you want to go');
Hope this helps!
In my react native app I save the user information securely on the key chain, so that after they have logged in once, I save the information and then the next time the user comes, the information is already there and so the user won't need to log in.
The issue is that I do the check in componentDidMount, and then if the user has never logged in before or logged out in their last visit I redirect them to the loginScreen like so:
componentDidMount() {
//Need to check if they've signed in before by checking the USER_INFO.
SecureStore.getItemAsync("USER_INFO").then(response => {
//Have they signed in before?
if (response !== undefined) {
//yes.
//continue with app stuff.
}
else {
//Not logged in before need to go to login.
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Login', params: this.props.navigation.state.params }),
]
});
this.props.navigation.dispatch(resetAction);
}
});
}
The problem is that I get a warning that '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.'. Which makes sense because I am redirecting before the screen has rendered, but then the question is, where should I perform these checks?
Thanks
I see that you are using react-navigation. I have done the same thing that you are trying to accomplish but in a different way.
To simplify I have three screens in a navigator
// Navigator.js
export const RootNavigator = StackNavigator({
Splash: { screen: SplashView },
Login: { screen: LoginView },
RootDrawer: { screen: RootDrawer }
}, {
headerMode: 'none',
initialRouteName: 'Splash'
});
And then in my SplashView (which is my starting point) I authenticate the user in its constructor. And while it is authenticating the user, the SplashView is simply rendering a Text that says "Splash Screen" but could obviously be anything.
constructor(props) {
super(props);
this.authenticateSession();
}
authenticateSession() {
const { navigation } = this.props;
dispatch(storeGetAccount())
.then(() => {
navigation.dispatch(navigateToAccountView(true));
})
.catch(() => {
navigation.dispatch(navigateToLoginView());
});
}
The functions navigateToAccountView() and navigateToLoginView() are just so I can use them at other places but the navigateToLoginView() looks like this
export function navigateToLoginView() {
return NavigationActions.reset({
index: 0,
key: null,
actions: [
NavigationActions.navigate({ routeName: 'Login' })
]
});
}
Usually and as far as I know, the best way to handle this kind of checks is by wrapping your component by some HOC(High Order Component) Doing your logic there, and depending if the user passes the checks you can throw a redirection to login page or load the user data and keep forward rendering your component.
This is a good practice so you can create a withAuth() HOC that will wrap the components or the parts of your app that can only be accessed by authenticated users. And you will have a component that is highly reusable.
So you will export your "protected component" like this:
export default withAuth(myComponent)
performing the logic in the withAuth HOC instead of in you component.