How to dynamically show Drawer items in React-Navigation V5? - reactjs

I am having a bit of trouble trying to dynamically show different items at React-Navigation drawer.
I have a drawer that needs to show different items if the user is logged in or not. In this case, if the user is logged in, the buttons for "RegisterScreen" and "LoginScreen" should NOT be shown and the "Log Out" button should be shown. However, if the user is NOT logged in, then both Register and Login screens should be shown and the Log out button should not.
I have successfully shown the buttons using the code below (Except for the Logout button, I did not do it yet). However, once the user logs out, the app redirects them to the login screen. Since I did not declare them in the drawer, I get the following error:
console.error: The action 'NAVIGATE' with payload {"name":"LoginModal"} was not handled by any navigator.
I do understand that the screens were not added, but I have no idea how they can be hidden when the user is logged in.
Code:
//Added DrawerItemList for the Logout button.
const CustomDrawerContent = (props) => {
return (
<DrawerContentScrollView {...props}>
<DrawerItemList {...props} />
<DrawerItem
label="Logout"
onPress={async () => {
props.navigation.closeDrawer();
await LogOut().then((result) => {
props.navigation.navigate('LoginModal');
});
}}
/>
</DrawerContentScrollView>
);
};
const [drawerCategories, setDrawerCategories] = useState([]);
//Checks if the user is logged in and sets Login and Register screens in the state.
useEffect(() => {
let mounted = true;
if (mounted) {
AsyncStorage.getItem('userId').then((result) => {
console.log(result);
if (result == null) {
setDrawerCategories([
...drawerCategories,
{name: 'LoginModal', component: {LoginScreen}},
{name: 'RegisterModal', component: {RegisterScreen}},
]);
}
});
}
return () => (mounted = false);
}, []);
return (
<NavigationContainer>
<RootDrawer.Navigator
drawerType="front"
initialRouteName="Home"
mode="modal"
drawerContent={(props) => <CustomDrawerContent {...props} />}>
<RootDrawer.Screen name="Home" component={MainStackScreen} />
//Add the Register and Login buttons here
{drawerCategories.map((drawer) => {
<RootDrawer.Screen name={drawer.name} component={drawer.component} />;
})}
</RootDrawer.Navigator>
</NavigationContainer>
);
Can you please help me figuring out how to fix this error? Thank you so much!

Not sure what your LogOut function does, but unless it actually re-runs the effect so that your LoginModal screen is defined, there won't be a screen named LoginModal to navigate to.
Looking at your code, your effect won't re-run after you logout. Instead of setting the list of screens in the effect, you should arrange your code to something like this:
Global store like Redux, React Context or whatever you prefer to store the userId
The useEffect hook should restore try to restore the userId from AsyncStorage like you're doing now, but after restoring, it should update your global store to update the userId there instead of setting the screens
Your logOut function should delete the userId from AsyncStorage as well as update the global store so that it's null.
Inside your component, you should conditionally define the screens based on if userId is null, and they'll automatically change on login or logout
The docs for authentication flow show an example of how this works using a token and stack navigator: https://reactnavigation.org/docs/auth-flow/
You can use similar strategy with an user id and a drawer navigator.

Related

How to reload a component in a bottom tab navigator

I have 4 bottom tab navigator options
<Tab.Screen name="Home" component={HomeScreen} options={{headerShown:false}}/>
<Tab.Screen name="Appointments" component={Doctors}/>
<Tab.Screen name="Lab Tests" component={Diagnostics}/>
<Tab.Screen name="Account" component={Profile} options={{ unmountOnBlur: true }}
In the Account tab, I am showing the Profile Details And Edit Profile option.
Clicking on Edit Profile I go to Edit Profile Page, Edit Save,
const Save = navigation.navigate("Account")
After hitting Save, I am returned to the Account tab but the component Profile, which I am using as an Account tab component, is not reloading, so the Profile Details I am using are still the old Details.
As You Can see I already used unmountOnBlur : true, It works only when I am switching tabs, I want the same behavior when I came back from the Edit Profile page to the Account Tab.
If your getting the profile data from an API's that means your calling the same API in the Account Tab as well. You can add an event listener and every time your screen get into focus you can call that apis and get the updated result.
Ref: https://reactnavigation.org/docs/navigation-events.
Example:
React.useEffect(() => {
const unsubscribe = props.navigation.addListener('focus', () => {
// call your api here
});
return unsubscribe;
}, [navigation]);
Without a complete example, it's hard to figure out the exact issue. Maybe the profile component is using the useEffect without deps will not call again.
You can Call a function when focused screen changes or re-render a screen with the useIsFocused hook -
useIsFocused - this will cause a re-render. May introduce unnecessary component re-renders as a screen comes in and out of focus.
import * as React from 'react';
import { Text } from 'react-native';
import { useIsFocused } from '#react-navigation/native';
function Profile() {
// This hook returns `true` if the screen is focused, `false` otherwise
const isFocused = useIsFocused();
......
}
Or Triggering an action with a focus event listener​, you can control the re-renders
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
// screen is focused
});
// Return the function to unsubscribe from the event so it gets removed on unmount
return unsubscribe;
}, [navigation]);
There's also a useFocusEffect.

React-Native - Return to Sign in screen if unauthenticated user

I am new to react/native and this is my first app.
I want to protect some screens which only an authenticated user can access. I read some articles and Questions. But I am still not able to get them either because they are class based or some for react.
I made a few attempts at it. One of them that seemed to work is like this.
OrderList.js
import IsUserLoggedIn from '../utilities/authHelpers';
import UserSignin from './UserSignin';
const OrderList = ({ route, navigation }) => {
...
...
if(!IsUserLoggedIn){
return <UserSignin/>
}
return (
<SafeAreaView style={styles.container}>
{renderOrders()}
</SafeAreaView>
)
}
export default OrderList;
This shows Signin screen. But it also runs all other logic like REST API calls, that is made in useEffect before IsLoggedIn check returns the Singin screen. I tried putting the check before those calls but then react-native complaints about returning too early. I also tried, before the useEffect etc, the navigation to signin route. That again didn't work. React-native throws warning that it cannot redirect from one component to another while it is still rendering. I don't recall exact message but it along those lines.
Another issue with this is that I will need to add this check in each of the protected screens. I want something that sits before all screens like in Navigators.
navigators/drawer.js
const Drawer = createDrawerNavigator();
const DrawerNav = () => {
return (
<Drawer.Navigator initialRouteName="Home"
>
<Drawer.Screen name="Home" component={StackNav} />
<Drawer.Screen name="Sign In" component={UserSignin} />
<Drawer.Screen name="My Orders" component={OrderList} />
<Drawer.Screen name="Profile" component={UserProfile} />
<Drawer.Screen name="Change Password" component={ChangePassword} />
<Drawer.Screen name="Forgot Password" component={ForgotPassword}/>
<Drawer.Screen name="Sign Up" component={UserSignup}/>
</Drawer.Navigator>
)
}
export default DrawerNav;
I have read about HOC(High Order Components) that some have suggested in this case. But I don't understand how that is implemented in this case.
Any help will be appreciated.
UPDATE 2
I finally got it working with few changes.
The main thing is that you need to make sure component are re-rendered. This happens only when props or state is updated. But in my case, I save authentication tokens in local storage and that doesn't affect state of the app, thus no re-rendering. Local state in drawer.js had no effect except the first time app started. If user signed in they need to refresh the app to make drawer get new state.
So I put another state in redux store. It is global store and I update it whenever a user is successfully logged in.
drawer.js
import {useSelector} from "react-redux";
import { OrderList, RequireAuthentication } from "../screens"
const DrawerNav = () => {
const isLoggedIn = useSelector(state => state.userSigninState?.user_is_signed_in)
return (
<Drawer.Navigator
initialRouteName="Home"
>
<Drawer.Screen name="My Orders" component={RequireAuthentication(OrderList, isLoggedIn)} />
)
}
export default DrawerNav;
RequireAuthentication.js
import UserSignin from './UserSignin';
const RequireAuthentication = (protectedScreen, isLoggedIn) => {
// This is a HOC()Higher Order Component).
// It will be used to make sure protected screens are not accessible to anyone accept authenticated user.
return (
isLoggedIn!=true?UserSignin:protectedScreen
)
}
export default RequireAuthentication;
reducers.js
const initialUserSignin = {
user_is_signed_in:false
}
export function userSigninReducer(state=initialUserSignin, action){
if(action.type == USER_SIGNIN_STATUS_UPDATED){
return {
...state,
user_is_signed_in: action.payload.is_signed_in,
};
} else {
return state
}
}
actions.js
export function userSigninStatusUpdated(user_signed_in){
return {
type:USER_SIGNIN_STATUS_UPDATED,
payload:{
is_signed_in:user_signed_in
}
}
}
Signin.js
import store from "../redux/store";
import {userSigninStatusUpdated} from '../redux/actions';
// This is when user signs in. We save their tokens and also update their signin state to true
await AsyncStorage.setItem('access_token', response.data.access);
await AsyncStorage.setItem('refresh_token', response.data.refresh);
store.dispatch(userSigninStatusUpdated(true));
In signout logic you will set the state to false like this:
store.dispatch(userSigninStatusUpdated(false));
I haven't implemented signout logic yet but you would also want to remove the tokens on signout.
Hope this helps someone.
UPDATE 1
This doesn't work. The issue is the async nature of IsUserLoggedIn(). I will try to fix that and post an update.
Original Answer
Answering my own question.
Screens or components are just functions. An HOC(High Order Component) is also a function but it can take another function(screen or component) as parameter or return another function.
So we create an HOC(a function) and pass to it our protected screen(a function) as parameter.
Inside our HOC we check if the user is logged in. If they are then we return the screen(that we passed as parameter to HOC) else we return Signin screen(again a function).
That is the main logic. I want this check to happen in navigators. So instead of having our protected screen directly mention in the params for the navigator we mention our HOC with our screen passed to that as param.
Below is my code .
RequireAuthentication.js // This is inside my screens directory.
import React from "react";
import IsUserLoggedIn from '../utilities/authHelpers'
import UserSignin from './UserSignin';
const RequireAuthentication = (protectedScreen) => {
// This is an HOC(Higher Order Component).
// It will be used to make sure protected screens are not accessible to anyone accept authenticated user.
return (
!IsUserLoggedIn()? UserSignin: protectedScreen
)
}
export default RequireAuthentication;
The navigator, drawer.js
import {RequireAuthentication } from "../screens"
const DrawerNav = () => {
return (
<Drawer.Navigator initialRouteName="Home"
>
<Drawer.Screen name="My Orders" component={RequireAuthentication(OrderList)} />
</Drawer.Navigator>
)
}
Thank you

How to navigate between pages in react with hooks

I'm a beginner at react hooks and I am trying to assign a state to every page of my module and set the initial state to an intro page and change state (page) on clicking next button.
This is how my main payment page looks
const [portalStage, setPortalStage] = useState("INTRO");
const Screens = {
INTRO: <IntroScreen />,
Post_INTRO: <Payment />, }
useEffect(() => {
setScreen();
}, [context]);
const setScreen = async () => {
setPortalStage("Post_INTRO");
}
{Screens[portalStage]}
<IntroScreen onClick = {setScreen} />
Now I am trying to add an Intro page to it which should always open first and then on clicking the next button on the introscreen it should redirect to the main component.
const IntroScreen = () => {
return (
<Wrapper>
<Content>
<h1>Coupons only!</h1>
<p>Increase your eligibility limit today!</p>
<Button>
Next
</Button>
</Content>
</Wrapper>
);
};
export default IntroScreen;
With this approach I can only see the main page and not the introscreen. What am I doing wrong in assigning state to both screens
A clean way to do this if you dont want use different routes would be to re-render when the next button is clicked. Some thing like below:
MainPaymentPage.js
const MainPaymentPage =(props)=>{
const [isNextClicked,setNextClicked]=useState(false);
let componentCode=<IntroScreen click={()=>setNextClicked(true)}/>
if(isNextClicked){
componentCode=<Payment/>
}
return(
{componentCode}
)
}
export default MainPaymentPage;
and add a click listener in your IntroScreen component like below:
const IntroScreen = (props) => {
return (
<Wrapper>
<Content>
<h1>Coupons only!</h1>
<p>Increase your eligibility limit today!</p>
<Button onClick={props.click}>
Next
</Button>
</Content>
</Wrapper>
);
};
export default IntroScreen;
you will have to make a similar change in your button component so that it can handle the click event. If the Button component comes from a framework like MaterialUI or Bootstrap, it should be able to handle it on its own, but you might have to rename the listener from onClick to whatever your framework wants.
The way the above code works is that there is now a parent component which has a state deciding which component to display based on the state value(isNextClicked, in this case). Initially, it will be set to false, causing the componentCode variable to be set to the code for IntroScreen. When the next button is clicked in the intro screen, it will change the state of the parent component(MainPaymentPage, in this case) to true, causing a re-render. This time, since isNextClicked is true, componentCode will be set to the code for your Payment component.

React app, API call that fetches routes for app

Hello I am using react and redux, i have my action creator that fetches the routes of the page and i creat the routing with them this way:
First in app.js im calling the action creator (using mapDispatchToProps) in UseEffect and passing the result (mapStateToProps) to the Routes component:
useEffect(() => {
fetchMenu();
}, []);
<Routes menu={menu} />
Then in Routes.js:
{menu.map((item) => (
<PrivateRoute
key={item.id}
exact
path={item.link}
parentClass="theme-1"
component={(props) => selectPage(item.linkText, props)}
/>
))}
The problem is that if I refresh the page, there is a little delay between the api call and the render of the page, so for one second the browser shows "NOT FOUND PAGE" and then instantly redirect to the route. How can I make it work properly? Thank you !
Basically what you want is to be able to know that the data hasn't been loaded yet, and render differently based on that. A simple check would be see if the menu is empty. Something like this:
export const Menu = ({ menu, fetchMenu }) => {
useEffect(() => {
fetchMenu();
}, []);
if ( menu.length > 0 ) {
return <Routes menu={menu} />
} else {
return <MenuLoading />
}
}
A more advanced setup would be able to tell the difference between an empty menu due to an API error and a menu that's empty because it's still loading, but to do that you would need to store information about the status of the API call in the state.

React Ant Design Modal Method update on state change

I'm currently migrating to antd, and have a modal appear on a certain route (ie /userid/info). I'm able to achieve this if I use the antd Modal react component, but I'd like to be able to use the modal methods provided such as Modal.confirm,Modal.info and Modal.error as they offer nicer ui straight out of the box.
I'm running to multiple issues such as having the modal rendered multiple times (both initially and after pressing delete in the delete user case), and unable to make it change due to state (ie display loading bar until data arrives). This is what i've tried but it constantly renders new modals, ive tried something else but that never changed out of displaying <Loader /> even though isFetching was false. I'm not sure what else to try.
const UserInfoFC: React.FC<Props> = (props) => {
const user = props.user.id;
const [isFetching, setIsFetching] = React.useState<boolean>(true);
const [userInfo, setUserInfo] = React.useState<string>('');
const modal = Modal.info({
content: <Loader />,
title: 'User Info',
});
const displayModal = () => {
const renderInfo = (
<React.Fragment>
<p>display user.info</p>
</React.Fragment>
);
const fetchInfo = async () => {
try {
user = // some api calls
setUserInfo(user.info);
modal.update({ content: renderInfo })
} catch (error) {
// todo
}
setIsFetching(false);
};
fetchInfo();
};
displayModal();
return(<div />);
};
reference: https://ant.design/components/modal/#components-modal-demo-confirm
edit: here is a replication of one of the issues I face: https://codesandbox.io/embed/antd-reproduction-template-1jsy8
As mentioned in my comment, you can use a useEffect hook with an empty dependency array to run a function once when the component mounts. You can initiate an async call, wait for it to resolve and store the data in your state, and launch a modal with a second hook once the data arrives.
I made a sandbox here
Instead of going to /:id/info and routing to a component which would have returned an empty div but displayed a modal, I created a displayInfo component that displays a button and that controls the modal. I got rid of attempting to use routes for this.
What I have now is similar to the docs

Resources