Warning when switching to another screen if the user has authorization - react-native

When I open the application and if the isAuth variable is true, it means the user has authorization, then I want to go to the provider screen.
My authorization component.
export const LoginScreen: NavigationStackScreenComponent<NavigationParams> = observer(({ navigation }: NavigationParams) => {
useEffect(() => {
AuthState.checkAuthentication();
}, []);
if (AuthState.isAuth) {
navigation.navigate('Provider');
}
return <View style={styles.body}>{LoaderState.loading ? <LoaderComponent /> : <AuthComponent />}</View>;
});
My provider component.
export const ProvidersScreen: NavigationStackScreenComponent<NavigationParams> = observer(({ navigation }: NavigationParams) => {
useEffect(() => {
ProvidersState.setProviders();
}, []);
return (
<View style={styles.body}>
{LoaderState.loading ? (
<LoaderComponent />
) : (
<ItemListComponent itemsList={ProvidersState.providersList} />
)}
</View>
);
});
But I get a warning. I understand that it is associated with the react-navigation library.
How to switch to another screen using a conditional statement?

Functional components are basically the equivalent to the render() function in class components. You are calling navigate before the component gets render and that's a problem.
You can either call the navigate inside your useEffect or put the conditional logic in the parent view.
In the first case, navigate will be called AFTER the component renders
useEffect(() => {
AuthState.checkAuthentication();
if (AuthState.isAuth) {
navigation.navigate('Provider');
}
}, []);
so it will be visible for half a second and then change. If you'd like to avoid this, then go for the second option.
You can put the condition in the parent. Something like
<View>{AuthState.isAuth ? <ProvidersScreen /> : <LoginScreen />}</View>

Related

loading on navigating between screens

I'm new in RN. When I want to navigate between screens I create this function:
displayScreen2 = () => {
this.props.navigation.navigate("screen2")
}
and I call it in onPress={this.displayScreen2}
with TouchableOpacity or any Touchable when the user clicks he has to wait 1 second or 2 before displaying the screen. So what I want is to change the Touchable icon to an loader.
It's simple if I use a conditional rendering but I don't know how to do it now, when I have to change my state? Any suggestions?
this is my approach:
<TouchableOpacity
style={Styles.topButton}
onPress= {() => {
this.setState({loading: 'load'},
() => {
displayScoreListView()
// this.setState({loading: 'icone'})
}
)
}}
>
<Text style={Styles.scoreListButtonTextRed}>{this.state.loading}</Text>
that not work, tha state change but visualy not because if I return to the first screen I have 'load' in the text component
You could create a custom component wrapping whatever Touchable you prefer, I've used this technique in my production apps before. The button has it's own state which allows you to automatically display a loading indicator when necessary.
export class ButtonWorker extends Component {
state = {
working: false
}
onButtonPress = () => {
this.setState(
{ working: true },
() => {
this.props.onPress(this.onWorkFinished);
}
);
}
onWorkFinished = () => {
this.setState({ working: false });
}
render() {
return (
<TouchableOpacity>
{this.state.working ? (
<ActivityIndicator />
) : (
this.props.children
)}
</TouchableOpacity>
);
}
}
And then use it like a normal button with additional logic!
export class NavigationScreen extends Component {
navigate = (done) => {
// ... Asynchronous logic goes here
done();
this.props.navigation.navigate("Screen2");
}
render() {
return (
<Fragment>
{/* ... */}
<ButtonWorker onPress={this.navigate} />
</Frament>
);
}
}

Can not implement callback inside of useFocusEffect from React-navigation

I have React-native app with topTabNavigator with three tabs. And usually componentDidMount and componentWillUnmount lifecycle methods don't work when the user changes the tab. Therefore instead of them I decided to use for the side effects onWillFocus and onDidFocus from React-Navigation. And before 5th version of this great library https://reactnavigation.org/ it was possible to import NavigationEvents component and put it to the view with focused callbacks:
import { NavigationEvents } from 'react-navigation';
class MyTeamScreen {
const store = this.props.store;
const members = store.members;
return (
<View>
<NavigationEvents
onWillFocus={payload => store.getTeamMebers()}
onDidFocus={payload => store.dispose()}
/>
<MymebersList team={members} />
</View>
);
}
export default MyScreen;
But at the moment there is no like this way after the upgrade of react reactnavigation library, because NavigationEvents is deprecated. And only one way to use useFocusEffect. And this is my hook:
function FetchMembers(store) {
useFocusEffect(
React.useCallback(() => {
return () => store.getMembers();
}, [store])
);
return null;
}
Class component:
class MyTeamScreen {
const store = this.props.store;
const members = store.members;
return (
<View>
<FetchMembers store={store} />
<MymebersList team={members} />
</View>
);
}
But I'm getting the error:
And I checked the store was initialized inside of the hook, but it can not call a method from it, because it's undefined.
Can you tell me please what I'm doing wrong? Is it a good way to use react-navigation methods instead of lifecycles componentDidMount and componentWillUnmount? Or maybe you could recommend me please the better way how to implement side effect when the user is changing the tab?
Looking at your code, I'm curious (but not so sure) that you may refer to the incorrect props?
Would you mind trying this?
Because the first parameter of functional component is props. To refer to props.store, you can use object destructuring like this
function FetchMembers({store}) {
useFocusEffect(
React.useCallback(() => {
return () => store.getMembers();
}, [store])
);
return null;
}
or
function FetchMembers(props) {
useFocusEffect(
React.useCallback(() => {
return () => props.store.getMembers();
}, [props.store])
);
return null;
}

How to set navigation params from outside navigation stack?

I have a header in my app that needs to render a different button depending on whether or not the user has notifications. This is how I currently have it set up in my pages:
static navigationOptions = ({ navigation }) => {
return {
title: 'My Profile',
headerRight: () => (
<Button
type="clear"
icon={() => <Icon
type="material-community"
size={25}
name={UserProvider.bNotifications ? 'bell' : 'bell-outline'}
color={UserProvider.bNotifications ? COLORS.WARNING : COLORS.WHITE}
/>}
onPress={() => navigation.navigate('Notifications', null)}
/>
)
}
};
The problem is, if UserProvider.bNotifications changes value, the header button doesn't update unless the page is changed / rerendered.
I want to switch to use the navigation property that is passed into those navigationOptions, but I don't no how to access it from outside the navigation stack. UserProvider is a static class (NOT a component) so I can't access the navigation prop through the usual manner (or by using the useNavigation hook).
I do have a NavigationProvider class that has access to the NavigationContainer for the app. I use it to trigger navigation without components. Is there some way I can set the params on the navigation property using that same reference?
NavigationProvider:
import { NavigationActions } from 'react-navigation';
let _navigator;
function getNavigator() {
return _navigator;
}
function setTopLevelNavigator(navigatorRef) {
_navigator = navigatorRef;
}
function navigate(routeName, params) {
_navigator.dispatch(
NavigationActions.navigate({
routeName,
params,
})
);
}
function goBack() {
_navigator.dispatch(
NavigationActions.back()
);
}
export default {
navigate,
setTopLevelNavigator,
getNavigator,
goBack
};
The ref is set like this in my top level App component:
<AppContainer
ref={navigatorRef => {
console.log(navigatorRef.props.navigation);
NavigationProvider.setTopLevelNavigator(navigatorRef);
}}
/>
EDIT - UserProvider
This is just the gist of my UserProvider class, but should convey how it works.
export default class UserProvider {
private static _bNotifications: boolean;
static get bNotifications(): boolean {
if (!this.hasInitNotifications)
this.initNotficationWatch();
return this._bNotifications;
}
static set bNotifications(bNotifications: boolean) {
this._bNotifications = bNotifications;
}
static initNotficationWatch() {
//Firebase listener on notification
if(notifications){
this.bNotifications = true;
} else {
this.bNotifications = false;
}
}
}
How to set navigation params from outside navigation stack?
to do this you have to utilize the getParam method that comes from the navigation prop. the way I would di it would be to set a variable to a parameter that would equal to UserProvider.bNotifications.
static navigationOptions = ({ navigation }) => {
let notifications = navigation.getParam('notifications')
return {
title: 'My Profile',
headerRight: () => (
<Button
type="clear"
icon={() => <Icon
type="material-community"
size={25}
name={notifications ? 'bell' : 'bell-outline'}
color={notifications ? COLORS.WARNING : COLORS.WHITE}
/>}
onPress={() => navigation.navigate('Notifications', null)}
/>
)
}
};
you can set the param to an initial value by adding it as a second argument if needed navigation.getParam('paramName', 'param initial value')
To update the parameter you need to use the setParams method. For this you can use the useNavigation hook.
You can also do this inside of a functions instead of a element
// remember to const navigation = useNavigation()
<Button
title="Update param"
onPress={() => navigation.setParams({notifications: 'new value'})}
/>
you can also initiate the value this way... but I would recommend to initialize the value inside your navigationOptions
I haven't tested the code but it should work
RN DOCS: https://reactnavigation.org/docs/2.x/headers/

Flat List not re-rendering even when I change a state

Expo-cli: 2.2.0
React-Navigation: 2.18.0
I have the following two screens for React Navigation, where one is to input the form details and another is the screen where the user can either edit on the submissions or confirm.
My Input interface looks like this:
export default class PickDropInterface extends
React.Component<NavigationProps<>> {
this.state = { tasks: [],
}
onSubmit = () => { this.props.navigation.navigate("Confirmation",
{tasks: this.state.tasks, deleteItem: this.deleteItem.bind(this)}); }
deleteItem = (key) => { var filteredTasks =
this.state.tasks.filter(function (item) { return (item.key !==key);
});
render() {
return (
<ItemDetail onSubmit={this.onSubmit} /> ) }
My Confirmation screen looks like this:
export default class Confirmation extends React.Component<NavigationProps<>> {
this.state={
refresh: false,
}
deleteItem = (key) => {
this.props.navigation.state.params.deleteItem(key);
this.setState({
refresh: !this.state.refresh
})
}
_renderItem =({ item }) =>
(
<View style={styles.cardStyle}>
<Button
primary
label="Delete" onPress= {() => {this.deleteItem(item.key)}} /></View>
)
render() {
return (
<FlatList data={task}
renderItem= {this._renderItem}
keyExtractor= {(item) => item.key.toString()}
extraData={this.state} />
)
}
Expected Output:
The delete button to prompt refresh in the FlatList and show the new Task list.
Current Output:
https://www.youtube.com/watch?v=RmrurTBQpak&feature=youtu.be
I don't know why FlatList didn't re-render, but I found a much simpler solution to what I wanted.
I used conditional rendering instead and I kind of think it's the way to do it instead of navigating to the other screen.
What I did is:
I made a new state called 'orderComplete' and set it to false as default.
Whenever, it is 'false', I made inputInterface above to render whereas it was 'true', I made the above ConfirmationScreen render.
More on Conditional Rendering can be found in React's official documentation.
FlatList above works like a charm now.

Where to initialize data loading with react-navigation

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