Where to initialize data loading with react-navigation - react-native

I'm using react-navigation and here is my structure :
The root stack navigator :
export const Root = StackNavigator({
Index: {
screen: Index,
navigationOptions: ({ navigation }) => ({
}),
},
Cart: {
screen: Cart,
navigationOptions: ({ navigation }) => ({
title: 'Votre panier',
drawerLabel: 'Cart',
drawerIcon: ({ tintColor }) => <Icon theme={{ iconFamily: 'FontAwesome' }} size={26} name="shopping-basket" color={tintColor} />
}),
},
...
My structure looks like this :
StackNavigator (Root)
DrawerNavigator (Index)
TabNavigator
MyPage
MyPage (same page formatted with different datas)
...
So my question is, where do I load my data, initialize my application ? I need somewhere called once, called before the others pages.
The first page displayed in my application is the MyPage page. But as you can see, because of the TabNavigator, if I put my functions inside, it will be called many times.
Some will says in the splashscreen, but I'm using the main splashscreen component and I don't have many controls over it.
I thought about my App.js where we create the provider, but I don't think this is a good idea ?
const MyApp = () => {
//TODO We're loading the data here, I don't know if it's the good decision
ApplicationManager.loadData(store);
SplashScreen.hide();
return (
<Provider store={store}>
<Root/>
</Provider>
);
};
What is the good way to do it ?

class MyApp extends Component {
state = {
initialized: false
}
componentWillMount() {
// if this is a promise, otherwise pass a callback to call when it's done
ApplicationManager.loadData(store).then(() => {
this.setState({ initialized: true })
})
}
render() {
const { initialized } = this.state
if (!initialized) {
return <SplashScreen />
}
return (
<Provider store={store} >
<Root />
</Provider>
);
}
}

TabNavigator by default renders/loads all its child components at the same time, but if you set property lazy: true components will render only if you navigate. Which means your functions will not be called many times.
const Tabs = TabNavigator(
{
MyPage : {
screen: MyPage
},
MyPage2 : {
screen: MyPage,
}
}
},
{
lazy: true
}
);
If you use this structure and call fetching data inside of MyPage you can add logic in componentWillReceiveProps that will check is data already in store and/or is it changed before fetching new data. Calling your fetch functions from MyPage gives you the ability to pull fresh data on every page/screen visit or do "pull to refresh" if you need one.
You could also pull initial data in splashscreen time, I would just not recommend pulling all your app data, data for all screens, at that time since you probably don't need it all at once. You can do something like:
class MyApp extends Component {
state = {
initialized: false
}
componentWillMount() {
// if this is a promise, otherwise pass a callback to call when it's done
ApplicationManager.loadData(store).then(() => {
this.setState({ initialized: true })
})
}
render() {
const { initialized } = this.state
if (!initialized) {
return null
}
return (
<Provider store={store} >
<Root />
</Provider>
);
}
}
class Root extends Component {
componentDidMount() {
SplashScreen.hide();
}
...
}

You should do it in App.js or where you initialize your StackNavigator. If I were you, I would put a loading screen, which would get replaced by the StackNavigator structure once the data is ready.

I wouldn't do it in the App because you lose control. Sadly I haven't used react-navigation or redux but I see that the TabNavigator has a tabBarOnPress method, which I would use to trigger the loading. You can load every page data on demand.
https://reactnavigation.org/docs/navigators/tab#tabBarOnPress

Related

How do I create an embedded Stack navigator inside a React Native formSheet modal?

Like so:
I'm running react-navigation v4.
First you have to follow the tutorial on how to set up a react-navigation modal, the one that has a jump animation and doesn't look like the native formSheet. You have to set up a stack navigator with your root navigator as one child and the modal as another:
And it scales, because you can have more than one of these modals as children.
The code for this is the following:
const RootNavigator = createStackNavigator(
{
index: { screen: AppNavigator },
[NC.SCREEN_ROOM_INFO]: { screen: RoomInfoNavigator },
[NC.SCREEN_CHAT_CREATE]: { screen: RoomCreateNavigator },
[NC.SCREEN_CHAT_SEARCH]: { screen: ChatSearchNavigator },
[NC.SCREEN_CHAT_GIF_PICKER]: { screen: GifPickerNavigator }
},
{
mode: 'modal',
headerMode: 'none',
transitionConfig: () => ({
transitionSpec: {
duration: 0
}
}),
transparentCard: true
}
)
Then you need to implement these, from my example, 4 navigators that will be displayed as modals each like so:
// Here you'll specify the screens you'll navigate in this modal, starting from index.
const RoomInfoStack = createStackNavigator({
index: { screen: NavigableRoomInfo },
[NC.SCREEN_ROOM_ROSTER]: { screen: NavigableRoomRoster },
[NC.SCREEN_ROOM_NOTIFICATION_PREFERENCES]: { screen: NavigableRoomNotificationPreferences },
[NC.SCREEN_ROOM_EDIT]: { screen: NavigableRoomEdit }
})
type NavigationComponent<T = any> = {
navigation?: NavigationScreenProp<NavigationState, T>
}
type Props = NavigationComponent
// THIS code is from the react-navigation tutorial on how to make a react-navigation modal:
// https://reactnavigation.org/docs/4.x/custom-navigators/#extending-navigators
class RoomInfoNavigator extends React.Component<Props> {
static router = {
...RoomInfoStack.router,
getStateForAction: (action, lastState) => {
// check for custom actions and return a different navigation state.
return RoomInfoStack.router.getStateForAction(action, lastState)
}
}
constructor(props) {
super(props)
this.onClose = this.onClose.bind(this)
}
onClose() {
this.props.navigation?.goBack()
}
// And here is the trick, you'll render an always open RN formSheet
// and link its dismiss callbacks to the goBack action in react-navigation
// and render your stack as its children, redirecting the navigator var to it in props.
render() {
return (
<Modal
visible={true}
animationType={'slide'}
supportedOrientations={['portrait', 'landscape']}
presentationStyle={'formSheet'}
onRequestClose={() => this.onClose()}
onDismiss={() => this.onClose()}
>
<RoomInfoStack {...this.props} />
</Modal>
)
}
}
export { RoomInfoNavigator }
This export is what our root stack imported before. Then you just need to render the screens, I have a pattern that I do to extract the navigation params to props in case this screen is ever displayed without navigation:
const NavigableRoomInfo = (props: NavigationComponent<RoomInfoProps>) => {
const roomInfo = props.navigation!.state!.params!.roomInfo
const roomInfoFromStore = useRoomFromStore(roomInfo.id)
// Here I update the navigation params so the navigation bar also updates.
useEffect(() => {
props.navigation?.setParams({
roomInfo: roomInfoFromStore
})
}, [roomInfoFromStore])
// You can even specify a different Status bar color in case it's seen through modal view:
return (
<>
<StatusBar barStyle="default" />
<RoomInfo roomInfo={roomInfoFromStore} navigation={props.navigation} />
</>
)
}
Sources:
https://reactnavigation.org/docs/4.x/modal
https://reactnavigation.org/docs/4.x/custom-navigators/#extending-navigators

React-Navigation passing functions to screenProps

I am creating an app using React Native, Expo and React Navigation, and I don't know the correct method for passing props and functions around.
I have copied the Expo method for navigators that I will build on, but right now I just have App.js calling the following
AppNavigator -> MainSwitchNavigator -> HomeScreen
I have then wrapped the main exported App that Expo expects with the Amazon AWS Amplify HOC withAuthenticator. Now I can log in to my app and show hello world securely using Amazon's cognito service.
If I want to log out I can use the signout function bellow that I currently have on the props of App.
class App extends React.Component {
constructor(props) {
super(props);
this.signOut = this.signOut.bind(this);
}
signOut() {
Auth.signOut().then(() => {
this.props.onStateChange('signedOut', null);
console.log("signed out");
}).catch(e => {
console.log(e);
});
}
render () {
return (
<View style={styles.container}>
<AppNavigator screenProps={{
signOut: () => {this.signOut}
}}/>
</View>
);
}
}
export default withAuthenticator(App)
For now I just want to pass this function down to my Home Screen so I can add a button and log out.
AppNavigator
export default createAppContainer(
createSwitchNavigator({
// You could add another route here for authentication.
// Read more at https://reactnavigation.org/docs/en/auth-flow.html
Main: { screen: MainSwitchNavigator, params: { signOut: this.props.screenProps.signOut } },
},
{
initialRouteName: 'Main'
})
);
MainSwitchNavigator
export default createSwitchNavigator({
// Home: { screen: HomeScreen }
Home: { screen: HomeScreen, params: { signOut: this.props.navigation.params.signOut } }
},{
initialRouteName: 'Home'
});
HomeScreen
class HomeScreen extends Component {
render () {
return (
<View>
<Text>Main App Here</Text>
<Button
onPress={this.props.navigation.params.signOut()}
title="Sign Out"
/>
</View>
)};
}
export default HomeScreen;
At the moment I get the error
undefined is not an object (evaluating 'this.props.navigation')
<unknown>
MainSwitchNavigator.js
8:62
Which puts its at the point I'm trying to read the props passed in to MainSwitchNavigator.
So my question is, what is good practice on sharing functions and state with screens below the main App and how do I pass the signOut function down to the rest of my components?
=======================================================================
EDIT
I have since worked out how to use screenProps correctly for variables but not for functions. screenProps is passed down through navigators automatically to the screen. So I only have to pass it to the AppNavigator component once and I can access it in HomeScreen. However any functions are not passed down.
E.g for variables, if I modify App.js to pass text to the variable signOut and pass that to screenProps
class App extends React.Component {
constructor(props) {
super(props);
this.signOut = this.signOut.bind(this);
}
signOut() {
Auth.signOut().then(() => {
this.props.onStateChange('signedOut', null);
console.log("signed out");
}).catch(e => {
console.log(e);
});
}
render () {
return (
<View style={styles.container}>
<AppNavigator screenProps={{signOut: "some text"}}/>
</View>
);
}
}
HomeScreen will then show this as part of the this.props object.
class HomeScreen extends Component {
render () {
return (
<View>
<Text>Main App Here</Text>
<Button
onPress={console.log(JSON.stringify(this.props))}
// onPress={alert(this.props.screenProps.var3)}
title="Sign Out"
/>
</View>
)};
}
export default HomeScreen;
However if I pass a function to AppNavigator instead and do this
<AppNavigator screenProps={{signOut: () => {this.signOut}}}/>
screenProps does not get set and I can't access the function signOut.
The only thing I can get to work is this
<AppNavigator screenProps={this.signOut}/>
But what I need is to create a property of screenProps and pass this down. Any thoughts?
passing a function as a screenProp should be
<AppNavigator screenProps={{signOut: this.signOut}} />
then to use do
this.props.screenProps.signOut()
MainSwitchNavigator
export default createSwitchNavigator({
// Home: { screen: HomeScreen }
Home: { screen: HomeScreen }
},{
initialRouteName: 'Home'
});
to send data on navigate
this.props.navigation.navigate('Signout' , { name: 'John Doe', email: 'john#doe.com' })

React navigation undefined params

I'm trying to pass params into a new screen, and implemented it like mentioned here.
I have the following TouchableOpacity button.
<TouchableOpacity
onPress={() => {
this.props.navigation.navigate('SomeScreen', {
title: 'Title',
subTitle: 'Subtitle',
});
}}
>
On the other page (let's call it Somescreen), I have the following:
render() {
const { navigation } = this.props;
const title = navigation.getParam('title');
}
But title above is undefined:
{ params: undefined, routeName: "Somescreen", key: "id-xx" }
My rootStack:
const RootStack = createStackNavigator({
SomescreenA: { screen: SomescreenA },
SomescreenB: { screen: SomescreenB },
}, { headerMode: 'none' });
Why are my params undefined in a new screen?
If you face a situation where your target screen get undefined params, probably you have a nested navigation stack.
Here you have to pass params to the navigate method in this way:
navigation.navigate('Root', {
screen: 'Settings',
params: { user: 'jane' },
});
For more information read this page in the official docs:
https://reactnavigation.org/docs/nesting-navigators/#navigating-to-a-screen-in-a-nested-navigator
In my specific case, I was calling a nested navigator, so I had to manage how send those params to their specific screen, so I did this:
Send params this way...the regular way:
navigation.navigate(
'OrderNavigator',
{itemSelected},
);
Then, from navigator stack I did this:
const OrderNavigator = ({route: {params}}) => {
return (
<Stack.Navigator initialRouteName="Order">
<Stack.Screen name="Order" component={Order} options={{headerShown: false}} initialParams={params} />
</Stack.Navigator>
);
};
And that's it. Then from the screen I got them like this:
const Order = ({route}) => {
const {itemSelected} = route.params;
const {first_name, last_name} = itemSelected;
return (...)
}
I've, unfortunately, encountered cases where navigate(route, params, ...) wouldn't pass the params object, just like you did.
As a workaround, I use the other variant - navigate({routeName, params, action, key}) that you can find here. It always works.
The accepted answer workaround did not work for me, so apparently if you use children to render your component (in screen options) and pass route as a prop, it works
if you are on react navigation v6^ use the useRoute hook to access the params object
const route = useRoute();
useRoute is a hook that gives access to the route object. It's useful when you cannot pass the route prop into the component directly, or don't want to pass it in case of a deeply nested child.
below is an implementation of this
import { useNavigation, useRoute } from '#react-navigation/native';
import { Pressable, Text } from 'react-native';
function Screen1() {
const navigation = useNavigation();
return (
<Pressable
onPress={() => {
navigation.navigate('Screen2', { caption: 'hey' });
}}
>
<Text> Go to Screen 2 </Text>
</Pressable>
);
}
function Screen2() {
const route = useRoute();
return <Text>{route.params.caption}</Text>;
}

In a React-Native app, how to use SwitchNavigator (react-navigator) and react-redux?

I'm using react-navigation's SwitchNavigator to manage my navigation based on authentication state. When Authenticated, I want to use Redux to store data I'm fetching.
My SwitchNavigator looks like this
SwitchRouteConfig = {
AuthLoading: AuthLoadingScreen,
Authenticated: AppStackNavigator,
NotAuthenticated: AuthStackNavigator,
}
SwitchConfig = {
initialRouteName: 'AuthLoading',
}
export default createSwitchNavigator(SwitchRouteConfig, SwitchConfig);
My Authenticated navigation looks like this:
// App Bottom Tab Navigator
const AppTabRouteConfig = {
AddItem: { screen: AddItem },
Items: { screen: Items },
Map: { screen: Map },
Help: { screen: Help },
}
const AppTabConfig = { initialRouteName: 'Items',}
const AppTabNavigator = new createBottomTabNavigator(
AppTabRouteConfig,
AppTabConfig)
And in my Screen we have:
class Items extends React.Component {
constructor(props){
super(props);
this.state = {};
}
componentDidMount(){
this.props.getData(); //call our redux action
}
render() {
if(this.props.isLoading){
return(
<View>
<ActivityIndicator />
</View>
)
} else {
return (
<Provider store={store}>
<SafeAreaView>
<FlatList
data={this.props.dataSource.features}
renderItem={({ item }) =>
<TouchableWithoutFeedback>
<View style={styles.listContainer}>
<Text>{item.prop1}</Text>
<Text>{item.prop2}</Text>
</View>
</TouchableWithoutFeedback>
}
/>
</SafeAreaView>
</Provider>
)
}
}
}
function mapStateToProps(state, props) {
return {
isLoading: state.dataReducer.isLoading,
dataSource: state.dataReducer.dataSource
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(Actions, dispatch);
}
export default connect(mapStateToProps,
mapDispatchToProps)(Items)
When I'm not authenticated, that works fine. I can login. When I am authenticated, I get the following error:
Invariant Violation: Could not find "store"
in either the context or props of
Connect(Items)". Either wrap the root
component in a <Provider>, or explicitly pass
"store" as a prop to "Connect(Items)".
In the reading I've done today, all the samples have a single top-level component which they wrap with . So, I'm not understanding how you instantiate the store and manage Redux without that model.
I should mention two additional things:
The initial authenticated app screen worked fine before I started to implement Redux.
I'm not trying to manage the state with Redux, just application data.
Project started with Create-React-Native-App.
Thank you!
You have to wrap the root component (the switch navigator) inside Provider to make the store available to all container components.
const SwitchRouteConfig = {
AuthLoading: AuthLoadingScreen,
Authenticated: AppStackNavigator,
NotAuthenticated: AuthStackNavigator,
}
const SwitchConfig = {
initialRouteName: 'AuthLoading',
}
const Navigator = createSwitchNavigator(SwitchRouteConfig, SwitchConfig);
// You can use a stateless component here if you want
export default class Root extends React.PureComponent {
render() {
return (
<Provider store={store}>
<Navigator />
</Provider >
);
}
}

React-native-navigation Change state from another tabnavigator

I'm using react-navigation / TabNavigator, is there a way to change the state of a tab from another tab without using Redux or mobx?
Yes you can. It is a little complicated, a little hacky and probably has some side-effects but in theory you can do it. I have created a working example snack here.
In react-navigation you can set parameters for other screens using route's key.
When dispatching SetParams, the router will produce a new state that
has changed the params of a particular route, as identified by the key
params - object - required - New params to be merged into existing route params
key - string - required - Route key that should get the new params
Example
import { NavigationActions } from 'react-navigation'
const setParamsAction = NavigationActions.setParams({
params: { title: 'Hello' },
key: 'screen-123',
})
this.props.navigation.dispatch(setParamsAction)
For this to work you need to know key prop for the screen you want to pass parameter. Now this is the place we get messy. We can combine onNavigationStateChange and screenProps props to get the current stacks keys and then pass them as a property to the screen we are currently in.
Important Note: Because onNavigationStateChange is not fired when the app first launched this.state.keys will be an empty array. Because of that you need to do a initial navigate action.
Example
class App extends Component {
constructor(props) {
super(props);
this.state = {
keys: []
};
}
onNavigationChange = (prevState, currentState) => {
this.setState({
keys: currentState.routes
});
}
render() {
return(
<Navigation
onNavigationStateChange={this.onNavigationChange}
screenProps={{keys: this.state.keys}}
/>
);
}
}
And now we can use keys prop to get the key of the screen we need and then we can pass the required parameter.
class Tab1 extends Component {
onTextPress = () => {
if(this.props.screenProps.keys.length > 0) {
const Tab2Key = this.props.screenProps.keys.find((key) => (key.routeName === 'Tab2')).key;
const setParamsAction = NavigationActions.setParams({
params: { title: 'Some Value From Tab1' },
key: Tab2Key,
});
this.props.navigation.dispatch(setParamsAction);
}
}
render() {
const { params } = this.props.navigation.state;
return(
<View style={styles.container}>
<Text style={styles.paragraph} onPress={this.onTextPress}>{`I'm Tab1 Component`}</Text>
</View>
)
}
}
class Tab2 extends Component {
render() {
const { params } = this.props.navigation.state;
return(
<View style={styles.container}>
<Text style={styles.paragraph}>{`I'm Tab2 Component`}</Text>
<Text style={styles.paragraph}>{ params ? params.title : 'no-params-yet'}</Text>
</View>
)
}
}
Now that you can get new parameter from the navigation, you can use it as is in your screen or you can update your state in componentWillReceiveProps.
componentWillReceiveProps(nextProps) {
const { params } = nextProps.navigation.state;
if(this.props.navigation.state.params && params && this.props.navigation.state.params.title !== params.title) {
this.setState({ myStateTitle: params.title});
}
}
UPDATE
Now react-navigation supports listeners which you can use to detect focus or blur state of screen.
addListener - Subscribe to updates to navigation lifecycle
React Navigation emits events to screen components that subscribe to
them:
willBlur - the screen will be unfocused
willFocus - the screen will focus
didFocus - the screen focused (if there was a transition, the transition completed)
didBlur - the screen unfocused (if there was a transition, the transition completed)
Example from the docs
const didBlurSubscription = this.props.navigation.addListener(
'didBlur',
payload => {
console.debug('didBlur', payload);
}
);
// Remove the listener when you are done
didBlurSubscription.remove();
// Payload
{
action: { type: 'Navigation/COMPLETE_TRANSITION', key: 'StackRouterRoot' },
context: 'id-1518521010538-2:Navigation/COMPLETE_TRANSITION_Root',
lastState: undefined,
state: undefined,
type: 'didBlur',
};
If i understand what you want Its how i figure out to refresh prevous navigation screen. In my example I refresh images witch i took captured from camera:
Screen A
onPressCamera() {
const { navigate } = this.props.navigation;
navigate('CameraScreen', {
refreshImages: function (data) {
this.setState({images: this.state.images.concat(data)});
}.bind(this),
});
}
Screen B
takePicture() {
const {params = {}} = this.props.navigation.state;
this.camera.capture()
.then((data) => {
params.refreshImages([data]);
})
.catch(err => console.error(err));
}