Triggering useEffect on previous stack screen React Native - javascript

I have a react native project which shows a UserNavigator page consisting of two tabs, one to show active data and the second to show inactive data, all the data can be clicked to navigate to UserDetails page
The problem is when I tried to update the inactive data to active and then go back to the previous screen which is UserNavigator the data on UserNavigator will not be updated since useEffect is not triggered to fetch updated data
Tab Nav page named UserNavigator
export default function DataList() {
return (
<Tab.Navigator>
<Tab.Screen name="Active" component={ActiveData} />
<Tab.Screen name="Inactive" component={InactiveData} />
</Tab.Navigator>
)
}
useEffect on Active and Inactive page
React.useEffect(() => {
(async () => {
setLoading(true);
try {
await getUserData();
} catch (err) {
Alert.alert("", err?.response?.data?.message || err?.message);
}
setLoading(false);
})();
}, []);
function on UserDetails to set user status to active then go back to list
const onSetActive = async () => {
setModalVisible(!modalVisible)
setLoading(true)
try {
const body = {
_id: item._id,
status: "Active",
}
const response = await axiosInstance.patch(`/UserData/update`, body)
await getUserData()
Alert.alert(
"Success",
"User has been set to active!",
[
{
text: "OK",
onPress: () => navigation.navigate("UserNavigator")
}
]
);
}
catch (err) {
Alert.alert("", err?.response?.data?.message || err?.message);
}
setLoading(false)
}
I've been trying to use useIsFocused on Active and Inactive Tab
Top Import
import { useIsFocused } from "#react-navigation/native";
const isFocused = useIsFocused();
React.useEffect(() => {
(async () => {
setLoading(true);
try {
await getUserData();
} catch (err) {
Alert.alert("", err?.response?.data?.message || err?.message);
}
setLoading(false);
})();
}, [isFocused]);
But when I tried to navigate between tabs it will rerender the data even when there isn't any change to the data and it will take more time to navigate and wait for data to be fetched
Navigation.replace won't do since I navigate to UserNavigator page from Main Page if I do that when I press the back button from UserDetails it will go back to Main Page instead of UserNavigator
Is there any alternative solution to my problem? I've been thinking to pass props when navigating to UserNavigator after updating data to trigger useEffect but I don't know how

You can use your reducer engine to achieve it, you can change the variable on any page on the app and subscribe it in any page using useEffect like this example:
import React, {useEffect} from 'react';
import store from '../store';
const Page = () => {
const {isFocused} = store.getState().updateReducer;
useEffect(() => {
(async () => {
setLoading(true);
try {
await getUserData();
} catch (err) {
Alert.alert("", err?.response?.data?.message || err?.message);
}
setLoading(false);
})();
}, [isFocused]);
return (<View></View>
}
export default Page;

Related

React.js, Auth Component does not redirect properly

I have created this Auth Component and it works fine. Except that, It does not redirect if the unauthenticated user tries to visit /dashboard.
The backend upon receiving /api/me request, knows the user by having the cookie. So I have (Cookie-Session) Authentication technique.
export const UserContext = createContext();
const Auth = ({ children }) => {
const [user, setUser] = useState(null);
const [gotUser, setGotUser] = useState(false);
const navigate = useNavigate();
const getUser = async () => {
const res = await fetch('/api/me');
const data = await res.json();
setUser(data);
if (user) {
setGotUser(true);
}
};
useEffect(() => {
if (!gotUser) {
getUser();
}
}, [user, gotUser, navigate]);
if (!user) {
navigate('/login');
}
console.log(user);
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};
So the main issue is that no redirection done. Also, The user passed to the context is not updated properly. Maybe because I am confused about what to use in useEffect .
Any help is appreciated.
Issues
There are a couple issues:
The "unauthenticated" state matches the "I don't know yet" state (i.e. the initial state value) and the component is redirecting too early. It should wait until the user state is confirmed.
The navigate function is called as an unintentional side-effect directly in the body of the component. Either move the navigate call into a useEffect hook or render the Navigate component to issue the imperative navigation action.
Solution
Use an undefined initial user state and explicitly check that prior to issuing navigation action or rendering the UserContext.Provider component.
const Auth = ({ children }) => {
const [user, setUser] = useState(); // <-- initially undefined
const navigate = useNavigate();
const getUser = async () => {
try {
const res = await fetch('/api/me');
const data = await res.json();
setUser(data); // <-- ensure defined, i.e. user object or null value
} catch (error) {
// handler error, set error state, etc...
setUser(null); // <-- set to null for no user
}
};
useEffect(() => {
if (user === undefined) {
getUser();
}
}, [user]);
if (user === undefined) {
return null; // <-- or loading indicator, spinner, etc
}
// No either redirect to log user in or render context provider and app
return user
? <Navigate to="/login" replace />
: <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};
useEffect runs after your JSX is rendered, so as your code is made, on a page refresh this if (!user) that calls navigate('/login') will always pass, as before the useEffect does its work, user is null, that inital value you gave to useState. Yet it's not redirecting because navigate does not work inside JSX, it should be replaced with Navigate the component.
Also, in getUser, you have this if (user) juste after setUser(data), that wouldn't work well as user won't get updated immediately, as updating a state is an asynchronous task which takes effect after a re-redner .
To fix your problems you can add a checking state, return some loader while the user is being verified. Also you can optimise a little bit your code overall, like getting ride of that gotUser state:
export const UserContext = createContext();
const Auth = ({ children }) => {
const [user, setUser] = useState(null);
const [checking, setChecking] = useState(true);
const getUser = async () => {
try {
const res = await fetch("/api/me");
const data = await res.json();
setUser(data);
} catch (error) {
setUser(null);
} finally {
setChecking(false);
}
};
useEffect(() => {
if (!user) {
getUser();
}
}, [user]);
if (checking) {
return <p>Checking...</p>;
}
if (!user) {
return <Navigate to="/login" replace />
}
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};
export default Auth;

React Native trigger useEffect from changes in AsyncStorage

So I'm working on a react native authentication screen. I'm storing the token in AsyncStorage (yea I know it's not the best solution).
So what's happening is when I log in, the token is stored, but the getItem on my Authentication.js screen is not being triggered, and the profile screen is not being called.
If I log in and then manually refresh the app, I am redirected to the profile screen.
Login.js
function Login({navigation}) {
const [signIn, {data}] = useMutation(USER_SIGNIN_MUTATION);
const [userName, setUserName] = useState('');
const [password, setPassword] = useState('');
function handleLogIn() {
signIn({
variables: {
email: userName,
password: password,
},
});
}
useEffect(() => {
if (data != null) {
setToken();
}
});
const setToken = async () => {
try {
console.log('before');
await AsyncStorage.setItem('token', data.signIn.token);
console.log('after');
} catch (error) {
console.log(error);
}
};
return(
...
)
}
Authentication.js
function Authentication() {
const [localToken, setLocalToken] = useState(false);
useEffect(() => {
const fetchUser = async () => {
try {
console.log('before get');
const userData = await AsyncStorage.getItem('token');
if (userData !== null) {
setLocalToken(true);
}
} catch (error) {
console.log(error);
}
};
fetchUser();
}, [localToken]);
console.log(`auth screen - ${localToken}`);
return (
<NavigationContainer>
{localToken === true ? <ProfileStack /> : <AuthStack />}
</NavigationContainer>
);
}
export default Authentication;
also same happens with the logout function when fired. (the function runs, but I need to refresh the app to get back to the login screen)
Profile.js
function Profile({navigation}) {
function signOut() {
logOut();
}
const logOut = async () => {
try {
console.log('before clear');
await AsyncStorage.removeItem('token');
console.log('after clear');
} catch (error) {
console.log(error);
}
};
return (
...
)
}
I'm grateful for any insight on this.
useEffect(() => {
const fetchUser = async () => {
try {
console.log('before get');
const userData = await AsyncStorage.getItem('token');
if (userData !== null) {
setLocalToken(true);
}
} catch (error) {
console.log(error);
}
};
fetchUser();
}, [localToken]);
Here you added the localToken variable in the dependency array of the useEffect. So you are basically saying: run this effect only if the localToken variable changes. But you change that from within the effect only. So try to remove it and keep the dependency as []. This way the effect will run when the component is rendered.
About the fact that you have to refresh the page, it is important to understand why this happens.
<NavigationContainer>
{localToken === true ? <ProfileStack /> : <AuthStack />}
</NavigationContainer>
Here you are rendering ProfileStack or AuthStack based on the localToken value. When you logout, you remove the token from the AsyncStorage but this is not enough. You actually need to trigger a rerender in the Authentication component so the localToken is reevaluated. Basically, when you logout you also need to set setLocalToken(false). So you need to access setLocalToken function from the Profile component. You can pass this function as a prop or better you can use Context API

apollo's useQuery data does not update after client.resetStore()

I am having an issue with useQuery from #apollo/client
I have a Navbar component
const Navbar = () => {
const { loading, data, error } = useQuery(GET_CURRENT_USER);
console.log('navbar', data)
return <ReactStuff />
}
And I also have a UserProfile component which allows the user to logout via a button. When the user presses the button, this code is ran:
const {
getCurrentUser,
changePassword,
changePasswordData,
logoutUser,
} = useUsers();
const logout = () => {
logoutUser();
localStorage.removeItem("authToken");
props.history.push("/");
};
useUsers is a custom hook which houses the resetStore function:
const useUser = () => {
const { client, loading, error, data, refetch } = useQuery(GET_CURRENT_USER);
const logoutUser = () => {
console.log("firing logout user");
client
.resetStore()
.then((data) => {
console.log("here in reset store success");
})
.catch((err) => {
console.log("here in error");
}); // causes uncaught error when logging out.
};
return {
// other useUser functions
logoutUser
}
}
Now I can't for the life of me figure out why the Navbar component, does not get updated when logout is pressed.
I have a withAuth higher-order component which does the exact same query, and this works absolutely fine. The user is sent to the /login page, and If I was to console.log(data) it is updated as undefined - as expected.
import { GET_CURRENT_USER } from "../graphql/queries/user";
/**
* Using this HOC
* we can check to see if the user is authed
*/
const withAuth = (Component) => (props) => {
const history = useHistory();
const { loading, data, error } = useQuery(GET_CURRENT_USER);
if (error) console.warn(error);
if (loading) return null;
if (data && data.user) {
return <Component {...props} />;
}
if (!data) {
history.push("/login");
return "null";
}
return null;
};
For some reason, this useQuery inside Navbar is holding onto this stale data for some reason and I can't figure out how to make it work correctly.
Update 1:
I've changed the logoutUser function to use clearStore() and the same thing happens, the Navbar is not updated, but I am redirected to /login and withAuth is working as intended.
const logoutUser = () => {
client
.clearStore()
.then((data) => console.log(data)) // logs []
.catch((err) => console.log(err)); // no error because no refetch
};
You're not waiting for the store to reset, probably the redirection and storage clean up happen before the reset completes, try doing that once it has finished
const logout = () => {
logoutUser(() => {
localStorage.removeItem('authToken');
props.history.push('/');
});
};
const logoutUser = onLogout => {
console.log('firing logout user');
client
.resetStore()
.then(data => {
onLogout();
})
.catch(err => {
console.log('here in error');
}); // causes uncaught error when logging out.
};
Check: do you have only ONE ApolloClient instance?
In some cases, if you configuring ApolloClient in the custom class or file you can implicitly create multiple apolloClient instances, for example by 'import' statement. In that case you clearing only one of the caches you made.

Facebook Login keeps repeating asking permissions React Native?

I'm a newbie in React Native and struggling to make a Facebook login for my app.
I finished configuring all the requirements for my app, Firebase and Facebook for developers.
The thing is when I pressed login, at the first time, it kept repeating again the Login permission. Until the data receive the accessToken and id, my app might/might not navigate to the Main screen (login succeed). But just 1 second later, the screen prompted and showed the Login Manager again. And it keeps repeating that. I really don't know why.
Is it something wrong with my code?. I'm thinking it kept repeating because It must do that until it gets the data need ( promise resolved)
Here's the code:
import React, { useEffect, useState } from 'react';
import {
View,
ImageBackground,
StyleSheet,
Alert
} from 'react-native';
import {
Layout
} from '#ui-kitten/components';
import { GoogleSignin, GoogleSigninButton, statusCodes } from '#react-native-community/google-signin';
import { firebase } from '#react-native-firebase/auth';
import { LoginButton, LoginManager, AccessToken } from 'react-native-fbsdk';
const GoogleLogIn = (props) => {
const [userInfo, setUserInfo] = useState(null);
const [isLogIn, setIsLogIn] = useState(false);
// Facebook log in
const _facebookLogin = async () => {
try{
const result = await LoginManager.logInWithPermissions(['public_profile', 'email']);
console.log(result);
if(result.isCancelled){
console.log('Login is cancelled');
}else if(result.grantedPermissions){
console.log('DONE')
const data = await AccessToken.getCurrentAccessToken();
console.log(data);
const cred = firebase.auth.FacebookAuthProvider.credential(data.accessToken);
const firebaseCred = await firebase.auth().signInWithCredential(cred);
setIsLogIn(true);
setUserInfo(data.userID);
props.navigation.navigate('AppNavigator', {screen: 'Welcome'})
}
}catch(err){
console.log(err);
throw err;
}
}
return(
<View style={styles.background}>
<LoginButton
onLoginFinished={_facebookLogin}
onLogoutFinished={() => console.log('Logout!')}
/>
</View>
);
};
const styles = StyleSheet.create({
background: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})
export default GoogleLogIn;
Here's the weird behavior:
Error_repeating asking permissions for login Facebook
PLEASE HELP!
The useState function seems to cause rendering again and cause problems.
You can try use useCallback
useCallback will return a memoized version of the callback that
only changes if one of the dependencies has changed. This is useful
when passing callbacks to optimized child components that rely on
reference equality to prevent unnecessary renders (e.g.
shouldComponentUpdate).
Usage
const _facebookLogin = useCallback( async () => {
try{
const result = await LoginManager.logInWithPermissions(['public_profile', 'email']);
console.log(result);
if(result.isCancelled){
console.log('Login is cancelled');
}else if(result.grantedPermissions){
console.log('DONE')
const data = await AccessToken.getCurrentAccessToken();
console.log(data);
const cred = firebase.auth.FacebookAuthProvider.credential(data.accessToken);
const firebaseCred = await firebase.auth().signInWithCredential(cred);
setIsLogIn(true);
setUserInfo(data.userID);
props.navigation.navigate('AppNavigator', {screen: 'Welcome'})
}
}catch(err){
console.log(err);
throw err;
}
},[])
Here's the code I found it worked well. You can put this in a button for customizing
const facebookLogin = async () => {
try {
const result = await LoginManager.logInWithPermissions(['public_profile', 'email']).then(
(res) => {
if(res.isCancelled){
console.log('Something went wrong, please try again');
}else{
AccessToken.getCurrentAccessToken().then(
async (data) => {
console.log(data.accessToken.toString())
const cred = firebase.auth.FacebookAuthProvider.credential(data.accessToken);
const firebaseCred = await firebase.auth().signInWithCredential(cred);
setIsLogIn(true);
setUserInfo(data.userID);
props.navigation.navigate('AppNavigator', {screen: 'Welcome'})
}
)
}
}
)
} catch (err) {
console.log(err);
}
}

Functional component is not rerendering after state is changed

I would like to ask why my component is not rerendering after my state is changed and I need to refresh it (switch between routes) to see changes. Well, the interesting fact is that the first time when I click the delete button page (component) does not rerender but after I switch routes and come back the item is deleted and when I try to delete other items, it gets deleted instantly not like the first time.
This is my code:
import React, {useEffect, useState} from 'react';
import ApiFactory from '../mock';
import Editor from '../Editor';
import ProductCard from '../components/product-card/product-card';
import ProductEdit from '../components/product-edit/product-edit';
export default function Admin() {
const [items, setItems]= useState([]);
useEffect(() => {
getProducts();
}, [items]);
function getProducts() {
ApiFactory.getInstance().get('/api/products')
.then((res) => {
if(res.status == 200) {
setItems(res.data);
}
})
.catch((error) => { console.log(error)})
}
function handleDelete (productId) {
ApiFactory.getInstance().delete(`/api/products/${productId}`)
.then(()=> getProducts()
);
}
return (
<>
{
items.map((item, index) => {
console.log(item.id)
return <>
<div key={index}>
<ProductCard product={item}></ProductCard>
<button onClick={() => handleDelete(item.id)}>Delete</button>
</div>
</>
})
}
</>
);
}
I am quite new in React can anybody explain why it happens and how can I fix it?
I believe it's because of how you have useEffect set up.
change the useEffect to only make the GET API call once (on initial load):
useEffect(() => {
getProducts();
}, []); // remove the dependency here. You may have made an infinite loop here.
const getProducts = () => {
ApiFactory.getInstance().get('/api/products')
.then((res) => {
if(res.status == 200) {
setItems(res.data);
}
})
.catch((error) => { console.log(error)})
}
If you confirmed that the API call is handling your errors / successes (are you getting non 200 status codes ? those may not be handled)
Add error catching to handleDelete to make sure this call works.
const handleDelete = (productId) => {
ApiFactory.getInstance().delete(`/api/products/${productId}`)
.then(getProducts())
).catch((error) => { console.log(error)})
}
You may additionally do as another user suggested and move even more logic away from the API calls (not required though) to have state locally and not re-fetch data from the API.

Categories

Resources