In my React Native mobile application, I would like to authenticate my users using AWS Amplify Authentication and manage navigation between screens using React Navigation. I tried to implement everything "by the book", i.e. as per the docs and my App conceptually looks like this:
// import statements skipped
class FirstTabScreen extends React.Component {
render = () => {
{/* access some data passed in screenProps */}
const someData = this.props.screenProps.someData;
{/* TODO: how to access AWS Amplify auth data here ?!? */}
{/* this.props contains screenProps and navigation objects */}
console.log("this.props: ", this.props);
{/* this.props.authState is undefined */}
console.log("this.props.authState: ", this.props.authState);
return <View><Text>First Tab - some data: {someData}</Text></View>
};
}
class SecondTabScreen extends React.Component {
render = () => {
const otherData = this.props.screenProps.otherData;
return <View><Text>Second Tab - other data: {otherData}</Text></View>
};
}
const AppNavigator = createBottomTabNavigator({
FirstTab: { screen: FirstTabScreen },
SecondTab: { screen: SecondTabScreen },
});
const AppContainer = createAppContainer(AppNavigator);
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
someData: null,
otherData: null,
};
}
componentDidMount = () => {
this.setState({ someData: 'some data', otherData: 'other data' });
};
render = () => {
return (
<Authenticator>
<AppContainer
screenProps={{
someData: this.state.someData,
otherData: this.state.otherData,
}}
/>
</Authenticator>
);
};
}
export default App;
The code sample above skips a few important details; I have created a full Expo Snack.
The AWS Amplify Authenticator wraps the application and makes authentication data available to embedded components as e.g. this.props.authState; see docs.
My problem now is that I don't understand how to access this auth data in e.g. FirstTabScreen: In that component, this.props contains screenProps and navigation objects, but this.props.authState is undefined. With AppContainer and AppNavigator in between Authenticator and e.g. FirstTabScreen, I don't see how to pass auth data as props or transport them by some other means.
Furthermore, I just pass all data to all embedded components in screenProps; this does work, but not all screens require all data. How would I go about passing them only the data they actually require ? I know about passing parameters to routes, but all the code samples in there have a Button and use this.props.navigation.navigate(...) in the onPress handler. I don't have any buttons on those screens and don't need any - after all, navigating to a different tab is what the tabs in the bottom tab nav bar are for ?!?
Could anyone kindly shed some light on this for me ?
using screeProp is not encouraged on react navigation v3
I believe it's no longer encouraged to put navigation such as TabNavigator nested within a component. I'm not quite sure how to handle such cases gracefully in V3. I wanted to upgrade from V1 to V3 recently and gave up on the upgrade since I couldn't find enough info on what to do with the nested navigators I have.
another idea from esipavicius
do not wrap screens to stack navigator which include the tab navigator. Maybe you Can use navigationAction setState or setParams with params and navigator key. When You Can dispatch it. And automaticaly changes state with params to which scren you dispatched.
OPTION 1 - USING SCREEN PROP
Your option of passing screen props
class App extends React.Component {
componentDidMount = () => {
this.setState({ someData: 'some data', otherData: 'other data' });
};
render = () => {
return (
<Authenticator>
<AppContainer
screenProps={{ myProp: "test" }}
/>
</Authenticator>
);
};
}
OPTION 2 - USING THE REACT CONTEXT API
Using the Context api and wrapping it around your <AppContainer>.
class App extends React.Component {
render = () => {
return (
<FormContext.Provider value={this.state}>
<AppContainer />
</FormContext.Provider>
);
};
}
Then consume it in each one of your screens.
class FirstTabScreen extends React.Component {
render(){
return(
<FormContext.Consumer>
{ (context) => (
// your logic
)}
</FormContext.Consumer>
};
}
Option 3 - TAB NAVIGATOR OBJECT
You can pass a TabNavigatorObject
createBottomTabNavigator(RouteConfigs, BottomTabNavigatorConfig);
You can pass an extra option to the BottomTabNavigator in the BottomTabNavigatorConfig.
const BottomTabNavigator = StackNavigator(routesConfig, BottomTabNavigatorConfig)
The BottomTabNavigatorConfig is an option and you can check the api docs
{ initialRouteName: 'Home', navigationOptions: .... }
Option 4 - RETRIEVE PARAM FROM PARENT
as explained on github and in the api docs, you can retrieve the parent component and then use getParam.
const parent = navigation.dangerouslyGetParent();
const yourParam = parent.getParam('yourParamName', 'default value')
You can find many discussion about passing props with tab navigator on github
1) I would suggest to not use any logic like this outside navigator. If you want login, do something like:
const rootStack = createSwitchNavigator({
Login: { screen: LoginNavigator },
Main: createBottomTabNavigator({
FirstTab: { screen: FirstTabScreen },
SecondTab: { screen: SecondTabScreen },
});
})
It will give you more login possibilities and once you know user is loggedIn just navigate him to Main stack.
2) Use any store for app. For example react-navigation works great with redux. You can save all info there.
Try to not use screenProps for simple cases like this. Also you don't need to send authState to other screens, just save to store.
3) Once you want logout. Do: Auth.signOut(), clean store, and navigate to Login screen.
Try to follow these simple steps and you will be able to implement much more complicated things
Related
I've got a React Native app with React Navigation and I need to show or hide a tab based on Redux state. I'm using createBottomTabNavigator and have written this:
function createTabNavigator(props:TabNavigatorProps){
debugger;
const isClient = WHAT_TO_PUT_HERE??
const tabs = isClient
? {
//something
}
: {
//something else
}
return createBottomTabNavigator(
tabs,
{
defaultNavigationOptions: ({ navigation }) => ({
tabBarIcon: ... ,
tabBarLabel: ...,
}
tabBarComponent: (props) => customTabBar(props, isClient),
tabBarOptions: {
...
},
});
};
My createTabNavigator works the way as intended if I manually set isClient to true or false, though I need this from redux state.
I've also tried connecting it, and mapStateToProps get called with the correct state, however it doesn't call createTabNavigator again so state changes aren't updated to my view hierarchy.
How can I add/remove a tab (and actually re-render altogether as that tab also involves some custom styling/rendering of the whole bar) based on Redux state?
react-navigation does not provide any API to hide a tab from BottomTabNavigator.
However, it allows providing custom TabBarComponent. It also exposes BottomTabBar component which is the default TabBarComponent for BottomTabNavigator.
This BottomTabBar component takes a property getButtonComponent to get the component for rendering each tab. It is provided the tab details in argument and is expected to return component to be used for rendering that tab.
To achieve hiding of tabs dynamically, you provide your own function for getButtonComponent. Check the details of tab with your state to decide whether it should be visible or hidden. Return an 'empty component' if you want to hide it, else simply call the default.
/**** Custom TabBarComponent ****/
const BlackHole = (props) => []
let _TabBarComponent = (props) => {
const getButtonComponent = (arg) => {
const hide = props.hiddenTabs[arg.route.key];
if (hide) {
return BlackHole;
}
return props.getButtonComponent(arg)
}
return (<BottomTabBar {...props} getButtonComponent={getButtonComponent}/>);
};
const mapStateToProps = (state) => ({
hiddenTabs: state.hiddenTabs
});
const TabBarComponent = connect(mapStateToProps)(_TabBarComponent);
/**** Navigation ****/
const TabNavigator = createBottomTabNavigator({
Tab1: createStackNavigator({Screen1}),
Tab2: createStackNavigator({Screen2}),
Tab3: createStackNavigator({Screen3}),
}, {
tabBarComponent: props => (
<TabBarComponent {...props} style={{borderTopColor: '#605F60'}}/>
)
});
See this snack for POC: https://snack.expo.io/BJ80uiJUH
We had the same requirement but in react navigation we cannot have dynamic tab, as we have to define all the routes statically well in advance.
Check this answer, in which I have explained in detail about how we achieved it.
Also have mentioned other possible approach (which we have not done).
There is one more interesting article you might want to check on this.
Make sure you are not calling redux state inside a HOC Component ,
For state management ,you can try to render it inside a component , try this logic :
class TabIndex extends Component {
render() {
// coming from redux
let isClient= this.props.isClient;
return (<View>
{isClient?<TabIndexNavigator ifYouNeed={ isClient } />
:
null}
</View>)
}
}
module.exports = TabIndex
then import it normally in your main stackNavigator
const StackMain = StackNavigator(
{
TabIndex: {
screen: TabIndex
},
...
}
I have 3 stack navigator screens (Home, Item List, Item Detail -> same order) inside drawer navigator and all three screens are hooked up to redux store with connect() helper.
When I do navigate('ItemDetail') from ItemList, it navigates to the screen and immediately comes back to ItemList screen. I am not sure why.
Following is my navigation structure -
const AppStack = createStackNavigator(
{
Home: {
screen: HomeScreen
},
ItemList: {
screen: ItemListScreen
},
ItemDetail: {
screen: ItemDetailScreen
}
},
{
initialRouteName: 'Home'
}
);
const DrawerNav = createDrawerNavigator(
{
DrawerApp: AppStack
},
{
drawerPosition: 'right'
}
);
const SwitchStack = createSwitchNavigator(
{
Loading: Loading,
Auth: AuthStack,
App: DrawerNav
},
{
initialRouteName: 'Loading'
}
);
This is how my each navigation screen component looks -
export class ProviderListScreen extends Component {
render() {
const { navigation } = this.props;
// ItemList is hooked up to Redux via connect() helper
return <ItemList navigation={navigation} />;
}
On my ItemDetail component, I get the Item data through route params from ItemList screen and I also dispatch an action (To reset some part of the store state) in component did mount. As soon as I do that, previous screen (ItemList) is automatically rendered.
Inside item detail, I make API call to create booking for that item and the booking object is managed by redux. Once I land on the ItemDetail, I reset the booking object for new booking data.
Here is the snippet of ItemDetail's componentDidMount -
componentDidMount() {
this.props.resetItembooking();
}
I am not sure what is causing this behaviour. If I remove the ItemList screen and jump directly to ItemDetail screen from HomeScreen, this issue does not occur.
Any help is appreciated.
I had the exact same problem however I tried the answer given in the original post comments sections given by Awadhoot but this did not work for me.
For anyone still trying to solve this issue, ensure you do not have any recurring intervals setup. Therefore you should always clear intervals before navigating away:
clearInterval(this.intervalId);
In react-navigation 5 you can use the useIsFocused hook for that:
import { useIsFocused } from '#react-navigation/native';
// ...
function Profile() {
const isFocused = useIsFocused();
return <Text>{isFocused ? 'focused' : 'unfocused'}</Text>;
}
From the docs: https://reactnavigation.org/docs/use-is-focused/
I'm trying to use "this.props.navigation" in my main react native App but it is undefined. In my others components included with TabNavigator and ScreenNavigator it work.
My App.js
const MyTabView = TabNavigator(
{
Projects: { screen: Projects },
Explorer: { screen: Explorer },
Profil: { screen: Profil }, ...
}
);
const StackView = StackNavigator(
{
global: {
screen: MyTabView,
headerMode: "none",
header: null,
navigationOptions: {
header: null
}
},
TermsOfUse: { screen: TermsOfUse },
LegalNotice: { screen: LegalNotice },
Options: { screen: Options }, ...
}
);
export default class App extends React.PureComponent {
constructor (props) {
super(props);
console.log(this.props.navigation); // <---------- UNDEFINED
}
render() {
return (
<Provider store={store}>
<PersistGate
loading={<ActivityIndicator />}
persistor={persistor}>
<View style={styles.fullScreen}>
<StackView />
<Tutorial navigation={this.props.navigation} /> // <---------- this.props.navigation is UNDEFINED
</View>
</PersistGate>
</Provider>
);
}
}
navigation prop is only available on components that are in navigation stack or child components that are using withNavigation HOC.
Your App class is not included in the Navigator stack, due to which, the navigator class cannot provide any navigation props to it, that is why you are getting an undefined error because the navigation object doesn't exist.
Also if you need to open <Tutorial/> as a first screen, include it as the first element in the StackNavigator. That ways, it'll be always called first.
PS: You can additionally provide navigation={this.props.navigation} element to different components, which does not have to be included in the navigator class. For example, you need to open a dropdown element, which provides a sign out option that navigates to a home screen. In this use case, it'll be better to provide the navigation props as you have provided it in the question. But care needs to be taken, that the class providing the prop should have the object present
EDIT
According to your requirement, you need to display a tutorial for each screen that you open. You can do it via two approaches.
Your way - You can declare it in the stack navigator. But your <Tutorial/> component will contain multiple conditions to check for the route and display the appropriate data ( that's what I can think of right now ).
You can declare a common component that only shows the message in a modal. You can then declare an optional prop for each component, and in each of your components, you can check if this optional prop exists, you can show your common display <Tutorial/> component.
This is what I can think of currently. Will update if I have more thoughts :)
Hope this answers.
I'm new in React Native and trying create my first app. So I have a question:
I got 2 screens (using react-navigation). At first screen there is a render of app logo with spinner(from native-base) and fetch to the server at the same time. And I need to navigate to another screen only when fetch is over and responce is handled. All I found at react-navigation docs is a solution with button, but it's not my case. Also I don't want to use ActivityIndicatorIOS (app should be correct for Android).
Please help me understand what should I do?
Thanks a lot!
Just call the navigate function in then callback of the fetch or handle error appropriately in catch.
Every screen in react-navigation gets the navigation prop. You can use the navigate function to navigate to any screen. Something like this
class FirstScreen extends React.Component {
static navigationOptions = {
title: 'First',
}
componentDidMount(){
const {navigate} = this.props.navigation;
navigate('Second');
fetch('http://apiserver').then( (response) => {
navigate('Second');
});
}
render() {
return (
<AppLogo />
);
}
}
const AppNavigator = StackNavigator({
First: {
screen: FirstScreen,
},
Second: {
screen: SecondScreen,
},
});
I'm learning React Native and Redux and I've started using 3rd party libraries - specifically React Navigation
I've followed a tutorial on it Dan Parker's Medium Tutorial and I still can't get it working
My RootContainer of the app:
<PrimaryNavigation />
...
export default connect(null, mapDispatchToProps)(RootContainer)
PrimaryNavigation definition:
const mapStateToProps = (state) => {
return {
navigationState: state.primaryNav
}
}
class PrimaryNavigation extends React.Component {
render() {
return (
<PrimaryNav
navigation={
addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.navigationState
})
}
/>
)
}
}
export default connect(mapStateToProps)(PrimaryNavigation)
PrimaryNav definition:
const routeConfiguration = {
LoginScreen: {
screen: LoginScreen
},
MainContainer: {
screen: MainContainer
}
}
const PrimaryNav = StackNavigator(
routeConfiguration,
{
headerMode: 'none'
})
export default PrimaryNav
export const reducer = (state, action) => {
const newState = PrimaryNav.router.getStateForAction(action,state)
return newState || state;
}
My create store:
const rootReducer = combineReducers({
...
primaryNav: require('../Navigation/AppNavigation').reducer
})
return configureStore(rootReducer, rootSaga)
I get an error along the lines of:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. Check the render method of 'PrimaryNavigation'
My understanding so far is:
RootContainer is connected to a store - it holds a PrimaryNavigation
PrimaryNavigation contains a Navigator (PrimaryNav), it wraps the Navigator and passes it state
PrimaryNav is the actual Navigator - I've defined routes and default initializations
the reducer that handles PrimaryNav is just PrimaryNav.router.getStateForAction
Am I missing initial state? Am I not connecting it to Redux properly? Do I need to fire off a dispatch to go to the first screen?
Thanks
I think you are missing a few things in your code organization.
This is my effort to extend the github notetaker app done by Tyler Mcginnis. I updated the code to support latest versions of React and React Native, I also extended the app to use react-navigation supporting both iOS and Android. I added Redux to manage the store and used Redux-Sagas to handle asynchronous fetch of data.
https://github.com/ahmedkamohamed/react-native-navigation-redux-sagas
you can see full demo from my git : https://github.com/liuxiaocong/redux-with-react-navigation
use redux and react-navigation from react-native