React-native: conditional rendering with loginState - reactjs

I am trying to create a login for a React Native app.
But the conditional rendering is not working, really don't know what I am doing wrong. (I am new to react native)
The isLoggedIn state checks if the user has a valid refreshToken. The token class works only thing that isn't working is the rendering after logging in.
InsightsApiDriver
isValidRefreshToken() {
return (
this.refreshToken !== undefined &&
new Date().getTime() - this.refreshToken.datetime.getTime() <
1 * 60 * 1000
);
}
MainContainer
const [isLoading, setIsLoading] = useState(true);
const [isLoggedIn, setIsLoggedIn] = useState(undefined);
useEffect(() => {
SecureStore.getItemAsync("refreshToken")
.then((tokenJson) => (tokenJson ? JSON.parse(tokenJson) : undefined))
.then((tokenObj) => {
if (tokenObj !== undefined) {
tokenObj.datetime = new Date(tokenObj.datetime);
}
global.AppInsightDriver = new InsightsApiDriver(tokenObj);
setIsLoading(false);
setIsLoggedIn(global.AppInsightDriver.isValidRefreshToken());
});
}, []);
return (
<NavigationContainer>
{console.log(isLoggedIn)}
{isLoggedIn ? (
<Tab.Navigator
initialRouteName={routes.session}
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
switch (route.name) {
case routes.session:
iconName = focused ? "speedometer" : "speedometer-outline";
break;
case routes.settings:
iconName = focused ? "settings" : "settings-outline";
break;
case routes.about:
iconName = focused ? "help-circle" : "help-circle-outline";
break;
case routes.login:
iconName = focused ? "log-in" : "log-in-outline";
default:
iconName = focused ? "log-in" : "log-in-outline";
break;
}
return <Icon name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: "#FFC62C",
tabBarInactiveTintColor: "white",
headerStyle: { backgroundColor: "#1F2341" },
headerTitleAlign: "center",
headerTintColor: "#FFC62C",
headerTitle: () => (
<HeaderTitle
SmartgridoneLogo={SmartgridoneLogo}
routeName={route.name}
/>
),
tabBarStyle: {
backgroundColor: "#1F2341",
},
})}
>
<Tab.Screen name={routes.session} component={SessionScreen} />
<Tab.Screen name={routes.settings} component={SettingScreen} />
<Tab.Screen name={routes.about} component={AboutScreen} />
</Tab.Navigator>
) : (
<Tab.Navigator
initialRouteName={routes.login}
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
switch (route.name) {
case routes.session:
iconName = focused ? "speedometer" : "speedometer-outline";
break;
case routes.settings:
iconName = focused ? "settings" : "settings-outline";
break;
case routes.about:
iconName = focused ? "help-circle" : "help-circle-outline";
break;
case routes.login:
iconName = focused ? "log-in" : "log-in-outline";
default:
iconName = focused ? "log-in" : "log-in-outline";
break;
}
return <Icon name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: "#FFC62C",
tabBarInactiveTintColor: "white",
headerStyle: { backgroundColor: "#1F2341" },
headerTitleAlign: "center",
headerTintColor: "#FFC62C",
headerTitle: () => (
<HeaderTitle
SmartgridoneLogo={SmartgridoneLogo}
routeName={route.name}
/>
),
tabBarStyle: {
backgroundColor: "#1F2341",
},
})}
>
<Tab.Screen name={routes.login} component={LoginScreen} />
</Tab.Navigator>
)}
</NavigationContainer>
);

The useEffect hook which you're using is more or less acting like a componentDidMount method. Which means it only runs when the screen is first mounted. Initially when you run the app, the isLoggedIn state gets set as false. But after you're logged in, this state is not being updated or atleast you haven't mentioned this in the code. If you also mention the routes.login method then it'll be easier to debug. Second time when you refresh app, the mount method sets the isLoggedIn state as true because the token is valid. So there's your issue

You need to render the whole navigation container when user re-authenticated by adding isLoggedIn on useEffect dependencies array
useEffect(() => {
SecureStore.getItemAsync("refreshToken")
.then((tokenJson) => (tokenJson ? JSON.parse(tokenJson) : undefined))
.then((tokenObj) => {
if (tokenObj !== undefined) {
tokenObj.datetime = new Date(tokenObj.datetime);
}
global.AppInsightDriver = new InsightsApiDriver(tokenObj);
setIsLoading(false);
setIsLoggedIn(global.AppInsightDriver.isValidRefreshToken());
});
}, [isLoggedIn]);

SecureStore doesnt provides an event listener for value changes so whenever refreshToken changes you need to recalculate the value of isLoggedIn. That would mean prop drilling setIsLoggedIn down to every component where the token is changed, or opting to use some form of state management. (React.useContext is nice)
With that in mind you will be using the following code at least twice, so you might as well put it in its own file (let's call it updateLogin.js), and export it as a function
updateLogin.js
// pass setIsLoggedIn as a parameter
export default updateIsLoggedIn=setIsLoggedIn=>{
SecureStore.getItemAsync("refreshToken")
.then((tokenJson) => (tokenJson ? JSON.parse(tokenJson) : undefined))
.then((tokenObj) => {
if (tokenObj !== undefined) {
tokenObj.datetime = new Date(tokenObj.datetime);
}
global.AppInsightDriver = new InsightsApiDriver(tokenObj);
setIsLoading(false);
setIsLoggedIn(global.AppInsightDriver.isValidRefreshToken());
});
}
Now MainContainer becomes:
import updateLogin from '../helpers/updateLogin';
/*
.
.
.
*/
const [isLoading, setIsLoading] = useState(true);
const [isLoggedIn, setIsLoggedIn] = useState(undefined);
useEffect(() => {
updateLogin(setIsLoggedIn);
}, []);
Then in your LoginScreen in whatever function where you write refreshToken to SecureStore after the write is finished just called updateIsLoggedIn(setIsLoggedIn).
Dont forget to either prop drill setIsLoggedIn.

Related

How to jump to the specific initial tab route dynamically route name?

I have problem jump to the specific tab navigation base on my dynamic initial route name.
Here is the scenario:
If the user just want to login, The initial route of Auth tab navigation must be on ProductBrowsing
If the user first time in the application and user register in the app, The initial route of Auth tab navigation must be on AddProduct.
Problem: When I login the screen not changing the screen stuck on login the navigation replace is not working.
Handler Login:
const HandlerLoginBtn = async () => {
const user_data = {
mobile_number: username,
password: password
}
await dispatch(LoginAuthentication(user_data)).then((res) => {
if(res?.payload?.message == 'Login Success') {
if(redirection == 'ProductBrowsing') {
navigation.replace('ProductBrowsing')
}
else if(redirection == 'AddProduct') {
navigation.replace('AddProduct')
}
}
})
}
Auth Tab:
function AuthenticateTabNav() {
return (
<Tab.Navigator tabBarOptions={{
inactiveTintColor: '#032363',
labelStyle: { fontSize:12, fontWeight:'500'},
style: {
backgroundColor: '#ffff',
}
}}
>
<Tab.Screen
options={{
tabBarLabel: 'BUY',
tabBarIcon: ({ focused , color, size }) => (
focused ? (
<Image h="4" w="4"
source={require('./assets/tab_icons/buy/blue_buy.png')}></Image>
): (
<Image h="4" w="4"
source={require('./assets/tab_icons/buy/black_buy.png')}></Image>
)
),
}}
name="ProductBrowsing"
component={ProductBrowsingScreen}
/>
<Tab.Screen
options={{
tabBarLabel: 'SELL',
tabBarIcon: ({ focused , color, size }) => (
focused ? (
<Image h="4" w="4"
source={require('./assets/tab_icons/sell/blue_sell.png')}></Image>
): (
<Image h="4" w="4"
source={require('./assets/tab_icons/sell/black_sell.png')}></Image>
)
),
}}
name="AddProduct"
component={AddProductScreen}
/>
</Tab.Navigator>
)
}
Navigation:
const AuthenticatedNav = () => {
const {redirection} = useSelector((state) => state.user)
const [firstScreen, setFirstScreen] = useState('');
console.log(firstScreen)
useEffect(() => {
setFirstScreen(redirection)
}, [])
return (
<Stack.Navigator initialRouteName={firstScreen == '' ? 'ProductBrowsing' : 'AddProduct'} headerMode='none'>
<Stack.Screen component={AuthenticateTabNav} name={firstScreen == '' ? 'ProductBrowsing' : 'AddProduct' } />
</Stack.Navigator>
)
}
Redux State:
builder.addCase(LoginAuthentication.fulfilled, (state, action) => {
if(action.payload?.message == 'Invalid Username Or Password') {
state.auth_access = []
state.isLoggedIn = false
state.isLoading = false
state.redirection = ''
}else {
state.auth_access = action.payload
state.isLoggedIn = true
state.redirection = 'ProductBrowsing'
}
})
I've made a full sample here https://snack.expo.dev/O3vW24UCl
If I understand correctly, you want to navigate from one tab to another depending on the user login state.
Changing the initialRouteName on the main Stack is not useful as you are moving within the tabs in your case.
What you want to do is the same but on the Tab.Navigator.
const Tabs = () => {
const isUserLoggedIn = useSelector((state) => {
return state.isUserLoggedIn;
});
return (
<Tab.Navigator
initialRouteName={isUserLoggedIn ? "AddProduct" : "ProductBrowsing"}
>
<Tab.Screen
options={{
tabBarLabel: 'BUY',
}}
name="ProductBrowsing"
component={ProductBrowsingScreen}
/>
<Tab.Screen
options={{
tabBarLabel: 'SELL',
}}
name="AddProduct"
component={AddProductScreen}
/>
</Tab.Navigator>
);
};
Obviously, in the sample the state is not persisted.

Why isn't functional child component changing state on props change?

I have a form template with a popup as child component. The popup works to display response from server for a form query(ajax) to login/forgot-password, etc. and will disappear automatically after some time.
Now, for the first time popup works fine and displays response and disappears but if I try it again from same page (as query is sent as ajax and page reloading doesn't happen) the message gets updated but it won't appear.
And what I have concluded to be the problem is that state of child component(isShown) not updating on message update and I can't seem to solve this. Or it can be I am overlooking some other thing .
AppForm.jsx (parent template)
function MainContent(props) {
const { title, underTitle, children, buttonText, customHandler, bottomPart } = props;
const [banner, setBanner] = useState({code: null, msg: null})
const reqHandler = async () => {
customHandler().then((resp) => {
setBanner({code: 'success', msg: resp.data.msg})
}).catch((err) => {
setBanner({ code: 'error', msg: err.response.data.msg })
});
}
return (
<ThemeProvider theme={formTheme}>
<Container maxWidth="sm">
<AppFormPopup statusCode={banner.code} msg={banner.msg} />
<Box sx={{ mt: 7, mb: 12 }}>
<Paper>
{children} //displays the fields required by derived form page
<Button type='submit' sx={{ mt: 3, mb: 2 }} onClick={reqHandler}>
{buttonText}
</Button>
</Paper>
</Box>
</Container>
</ThemeProvider>
)
}
function AppForm(props) {
return (
<>
<AppFormNav />
<MainContent {...props} />
</>
);
}
AppForm.propTypes = {
children: PropTypes.node,
};
export default AppForm;
AppFormPopup.jsx (child Component which shows popup msg)
function MainContent(props){
const { msg } = props;
const [progress, setProgress] = useState(0);
const [isShown, setIsShown] = useState(true); //state used for controlling visibility of popup msg.
useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if(oldProgress === 100){
setIsShown(false);
// return 0;
}
return Math.min(oldProgress + 10, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
});
return (
<Grid container sx={{
display: isShown ? 'block' : 'none',
}}>
<Grid item sx={{ color: "white", pl: 1 }}> {msg} </Grid>
</Grid>
)
}
export default function AppFormPopup({msg}) {
if(msg === null) return;
return (
<MainContent msg={msg} />
)
}
ForgotPass.jsx (form page which derives form template AppForm.jsx)
export default function ForgotPass() {
const loc = useLocation().pathname;
const mailRef = useRef(null);
const reqHandler = async () => {
const email = mailRef.current.value;
if(email){
const resp = await axios.post(loc, {email})
return resp;
}
}
return (
<AppForm
buttonText="Send Reset Link"
customHandler={reqHandler}
>
<Box sx={{ mt: 6 }}>
<TextField
autoFocus
label="Email"
inputRef={mailRef}
/>
</Box>
</AppForm>
)
}
This is due to the fact that your useEffect will only render once. My suggestion would be to change your useEffect to render if a value change, for example: Let say you got a state name of UserName. Now each time that UserName is being change will rerender your UseEffect function. By adding a [UserName] at end of useEffect it will cause a rerender each time UserName change.
useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if(oldProgress === 100){
setIsShown(false);
// return 0;
}
return Math.min(oldProgress + 10, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
},[UserName]);
It feels weird answering your question but here we go.
So, thanks #GlenBowler his answer was somewhat right and nudged me towards the right answer.
Instead of modifying of that useEffect, we needed another useEffect to check for changes in msg. So, code should look like this:
AppFormPopup.jsx
function MainContent(props){
const { msg } = props;
const [progress, setProgress] = useState(0);
const [isShown, setIsShown] = useState(true); //state used for controlling visibility of popup msg.
useEffect(() => {
setIsShown(true);
setProgress(0);
}, [msg]);
useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if(oldProgress === 100){
setIsShown(false);
// return 0;
}
return Math.min(oldProgress + 10, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
});
return (
<Grid container sx={{
display: isShown ? 'block' : 'none',
}}>
<Grid item sx={{ color: "white", pl: 1 }}> {msg} </Grid>
</Grid>
)
}
export default function AppFormPopup({msg}) {
if(msg === null) return;
return (
<MainContent msg={msg} />
)
}

Getting "Can't perform a React state update on an unmounted component" only the first time

I am creating a ToDo app. This app has two screens: Todos and Done. I'm using BottomTabNavigator to switch between these screens. These two screens has list of todos. The todos component shows the undone todos and the Done component shows the done todos. There's a checkbox on the left and Trash icon on the right of every single todo. When a todo from Todos page is checked then it moves to the Done page. The issue is: after switching to the Done screen from Todos for the first time then after unchecking the todo there gives this warning:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in SingleTodo (at Done.tsx:74)
After this, the app is running perfectly. As I'm not sure which component is causing this error that'w why I'm sharing the minimal version of the code.
I have set up Bottom Tab navigator component like this:
import stuff..
...
const HomeTab = () => {
return (
<Tab.Navigator
screenOptions={({route}) => ({
headerShown: false,
tabBarIcon: ({focused, color, size}) => {
let iconName = '';
size = focused ? 25 : 20;
if (route.name === 'To-Do') {
iconName = 'clipboard-list';
} else if (route.name === 'Done') {
iconName = 'clipboard-check';
}
return <FontAwesome5Icon name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#0080ff',
tabBarInactiveTintColor: '#777777',
tabBarLabelStyle: {fontSize: 15, fontWeight: 'bold'},
})}>
<Tab.Screen name="To-Do" component={Todos} />
<Tab.Screen name="Done" component={Done} />
</Tab.Navigator>
);
};
export default HomeTab;
As you can see, there are 2 components here. One is Todos. The code for this component is as follows:
import stuff...
...
const Todos = ({navigation}) => {
const dispatch = useAppDispatch();
const {todos}: {todos: TodoInterface[]} = useAppSelector(
state => state.todoReducer,
);
useEffect(() => {
loadTodos();
}, []);
const loadTodos = () => {
AsyncStorage.getItem('todos').then(todos => {
const parsedTodos: TodoInterface[] = JSON.parse(todos || '{}');
dispatch(setAllTodo(parsedTodos));
});
};
return (
<HideKeyboard>
<View style={styles.body}>
<FlatList
data={todos.filter(todo => todo.done !== true)}
renderItem={({item, index}) => {
const firstChild = index == 0 ? {marginTop: 5} : {};
return (
<TouchableOpacity
style={[styles.todoWrp, firstChild]}
onPress={() => todoPressHandler(item.todoId)}>
<SingleTodo // ***The code for this one is given below***
title={item.title}
subtitle={item.subTitle}
done={item?.done}
todoId={item.todoId}
/>
</TouchableOpacity>
);
}}
keyExtractor={(item, i) => item.todoId}
/>
<TouchableOpacity style={styles.addBtn} onPress={addBtnHandler}>
<FontAwesome5 name="plus" color="#fff" size={25} />
</TouchableOpacity>
</View>
</HideKeyboard>
);
}
The code for SingleTodo is as follows:
const SingleTodo = ({title, subtitle, done: doneProp, todoId}: Props) => {
const [done, setDone] = useState(doneProp);
const dispatch = useAppDispatch();
const {todos}: TodosType = useAppSelector(state => state.todoReducer);
const checkBoxHandler = (val: boolean) => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList[index].done = val;
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
setDone(val);
});
};
const deleteHandler = () => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList.splice(index, 1);
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
});
};
return (
<View style={styles.body}>
<CheckBox
value={done}
onValueChange={val => checkBoxHandler(val)}
style={styles.checkbox}
/>
<View>
<Text style={[styles.title, GlobalStyle.IndieFont]}>{title}</Text>
<Text style={[styles.subtitle, GlobalStyle.IndieFont]}>{subtitle}</Text>
</View>
<View style={styles.trashWrp}>
<TouchableOpacity onPress={deleteHandler}>
<FontAwesome5Icon
style={styles.trashIcon}
name="trash"
color="#e74c3c"
size={20}
/>
</TouchableOpacity>
</View>
</View>
);
};
export default SingleTodo;
The code for Done component is similar to Todos component. The only changes is on the data property of the component
<FlatList
data={todos.filter(todo => todo.done === true)}
...
other props...
...
/>
It's happening every time you use this, it is just shown once to not spam the console.
const checkBoxHandler = (val: boolean) => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList[index].done = val;
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
setDone(val);
});
};
const deleteHandler = () => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList.splice(index, 1);
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
});
};
Basically, you call the function, and the todo is unmounted from the state, but the function is not completed yet and you get that warning.
The solution is to lift everything related to the deleteHandler and checkBoxHandler from your children (Todo) to your parent (Todos), and pass it to Todo as props. Since parent is always mounted, deleting the todo will not unmount the parent and therefore, delete function will not be interrupted.

Passing a param using initialParam to another screen but the value of the param received is empty

I've been working three weeks on a project using react-native, I've never used this langage before so it's still new for me.
When I pass "test" as a param to Publication screen, route.params.post displays the right value which is "test" but when I pass {user}, it displays an empty value (route.params.post = {}).
I have no problem when I pass {user} as param to the leftHeader and RightHeaderComponent, I receive the object which is not empty there.
I don't understand the issue here, why do I get an empty value when I pass {user} to another screen using initialParams ?
export default function AppRouterComponent() {
const [user, setUser] = useState({});
useEffect(() => {
getUser()
.then(data => {
return data;
})
.then(data => {
setUser(data);
})
.catch(err => {
console.log(123123);
});
}, []);
const dimensions = useWindowDimensions();
return (
<Drawer.Navigator
initialRouteName="home"
screenOptions={{
drawerType: dimensions.width >= 768 ? "permanent" : "front",
headerLeft: () => <LeftHeaderComponent object = {user}/>,
headerTitle: () => null,
headerRight: () => <RightHeaderComponent object = {user}/>,
}}
drawerContent={(props) => DrawnerWithHeader(props)}
>
<Drawer.Screen name="home" component={HomeScreen}/>
<Drawer.Screen name="View/Edit user info" component={UserInfo} initialParams={{ post: user}}/>
<Drawer.Screen name="user preferences" component={UserSettings} />
<Drawer.Screen name="Publication" component={PublicationScreen} initialParams={{ post: "test"}}/>
</Drawer.Navigator>
);
}
/*The other screen, when I use the debugger to check route.params.post value, it displays an empty object*/
export default function UserInformationScreen({navigation, route}) {
return (
<Card>
<UserInformationComponent object = {route.params.post}/>
</Card>
);
}

How to nest tab navigation under container in React Native?

How do I create tabbed navigations like the image shown? Using tabBarPosition it seems I can only set the tab bar to 'top' or 'bottom' of the entire screen? Is there any way to do something similar but within a container? I'm using https://reactnavigation.org/, but if you have other suggestions as well that'd be great (a library that is easy to use, and can allow me to apply custom styles for headers, buttons etc easily).
You might need custom navigator by yourself.
Here is the docs: https://reactnavigation.org/docs/navigators/custom
And here is the example how to do that:
https://github.com/react-community/react-navigation/blob/master/examples/NavigationPlayground/js/CustomTabs.js
Following #Tyreal Gray's advise, I created a custom navigator.
Here is some sample code.
const CustomTabBar = ({ navigation }) => {
let selectedIndex = navigation.state.index;
let isSelected = false;
const { routes } = navigation.state;
return (
<View style={styles.tabContainer}>
{routes.map((route, index) => {
selectedIndex === index ? isSelected = true : isSelected = false;
let textColor = isSelected ? 'blue' : 'gray';
return (
<TouchableOpacity
onPress={() => navigation.navigate(route.routeName)}
style={styles.tab}
key={route.routeName}
>
<Text style = {{color: textColor}}>{route.routeName}</Text>
</TouchableOpacity>
)
})}
</View>
);
};
const CustomTabView = ({ router, navigation }) => {
const { routes, index } = navigation.state;
const ActiveScreen = router.getComponentForState(navigation.state);
return (
<KeyboardAwareScrollView>
<View style={styles.container}>
<CustomTabBar navigation={navigation} />
<ActiveScreen
navigation={addNavigationHelpers({
...navigation,
state: routes[index],
})}
/>
</View>
</KeyboardAwareScrollView>
);
};
const CustomTabRouter = TabRouter(
{
Details: {
screen: DetailsScreen,
path: '',
},
Media: {
screen: MediaScreen,
path: 'media',
},
Reviews: {
screen: CreateReviewFlow,
path: 'reviews',
},
},
{
// Change this to start on a different tab
initialRouteName: 'Details',
}
);
const CustomTabs = createNavigationContainer(
createNavigator(CustomTabRouter)(CustomTabView)
);

Resources