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

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.

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;

Triggering useEffect on previous stack screen React Native

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;

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

props not being updated properly in Login page button action with React JS

I am working on a Login page where on click of submit button I do some backend api operation and return success/failure. I am able to receive a successful response in my mapStateToProps function but it is not reflected in then part of my api call.
Following is my mapStateToProps function:
Login.propTypes = {
isLoggedIn: PropTypes.bool.isRequired
}
const mapStateToProps = (state) => {
return {
isLoggedIn: state.auth.isLoggedIn
}
}
export default connect(mapStateToProps, {login})(Login);
My submit button action:
const handleLogin = (e) => {
e.preventDefault();
if (checkBtn.current.context._errors.length === 0) {
dispatch(login(username, password))
.then(() => {
setLoading(false);
if (props !=null && props.isLoggedIn) {
props.history.push("/home");
}
})
.catch(() => {
setLoading(false);
});
} else {
setLoading(false);
}
};
props.isLoggedIn is still false even when I receive it as true from the action. Although when I click on the submit button, the user is able to login. So I assume that state is somewhere updated but not reflected in the then function of API call.
I think you're checking the value of isLoggedIn before it actually gets changed by your action in the state (this operation runs asyncronously).
I'd suggest you to dispatch your login and then check the isLoggedIn separately in a useEffect, something like
const {isLoggedIn} = props;
const handleLogin = (e) => {
e.preventDefault();
if (checkBtn.current.context._errors.length === 0) {
dispatch(login(username, password))
.then(() => {
setLoading(false);
})
.catch(() => {
setLoading(false);
});
} else {
setLoading(false);
}
};
useEffect( () => {
// This hook will run every time isLoggedIn changes value
if (isLoggedIn) {
props.history.push("/home");
}
}, [isLoggedIn])

Firebase with useeffect cleanup function

I'm trying to get all data with the 'public' parameter of a firebase collection and then use useffect, but the console accuses error.
I took this structure from firebase's documentation:
https://firebase.google.com/docs/firestore/query-data/get-data#get_multiple_documents_from_a_collection
but the console says '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'
So I went to this other page:
https://firebase.google.com/docs/firestore/query-data/listen#detach_a_listener
But I'm not using onSnapshot, besides firebase documentation seems wrong to me, since unsub is not a function.
useEffect(() => {
let list = []
const db = firebase.firestore().collection('events')
let info = db.where('public', '==', 'public').get().then(snapshot => {
if (snapshot.empty) {
console.log('No matching documents.');
setLoading(false)
return;
}
snapshot.forEach(doc => {
console.log(doc.id, "=>", doc.data())
list.push({
id: doc.id,
...doc.data()
})
})
setEvents(list)
setLoading(false)
})
.catch(error => {
setLoading(false)
console.log('error => ', error)
})
return () => info
}, [])
You need to invoke the listener function to detach and unmount.
https://firebase.google.com/docs/firestore/query-data/listen#detach_a_listener
useEffect(() => {
let list = []
const db = firebase.firestore().collection('events')
let info = ...
return () => info() # invoke to detach listener and unmount with hook
}, [])
You don't need to clear a .get(), it's like a normal Fetch api call.
You need to make sure your component is still mounted when you setLoading
Example:
// import useRef
import { useRef } from 'react'
// At start of your component
const MyComponent = () =>{
const isMounted = useRef(false)
useEffect(() => {
isMounted.current = true;
return () => isMounted.current = false;
}, [])
// Your query
db.where('public', '==', 'public').get().then(snapshot => {
if(isMounted.current){
// set your state here.
}
})
return <div></div>
}

Categories

Resources