I am having a problem understanding how to implement:
sign a user in
keep listening to the user changes
sign a user out
unsubscribe to the listener
using :
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
// Signed in
} else {
// Signed out
}
});
unsubscribe();
I understand that the above listener is recommended but I don't understand how to use it. Can someone explain in more details?
I wanna know if this function should be called when I first sign a user in, but then when and how should I sign them out? Should I call this function in each screen of the app (for ex. in componentDidMount)?
Moreover, where should I unsubscribe() to the listener? In which screen? Let's say there are THREE screens: Login, Screen 1, Screen 2. I call the listener in Login screen, then when the user is in Screen 1 or Screen 2, how am I supposed to keep listening to the changes and sign him out when I should AND unsubscribe to the listener?
firebase.auth().onAuthStateChanged opens up a listener, so a good idea is to make a useEffect that has the user as input, and change the user state according to if you are signed in or not. Once you use the sign out method, the onAuthStateChange will trigger. Then you can pass down the user everywhere through props o any global state you like.
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user)
} else {
setUser(null)
}
});
}, [user])
I use react-query instead of useEffect, and then I get the user claims, just to give you another example.
const fetchClaims = async () => {
setLoading(true);
const fetchedUser = await auth.onAuthStateChanged((user) => {
if (user) {
user.getIdTokenResult().then((idTokenResult) => {
// console.log(idTokenResult.claims);
setIsClient(idTokenResult.claims.client);
setIsMaster(idTokenResult.claims.master);
setNickName(idTokenResult.claims.name);
setUserEmail(idTokenResult.claims.email);
setClientName(idTokenResult.claims.clientName);
setIsPremium(idTokenResult.claims.premium);
setUserPicture(idTokenResult.claims.picture);
});
setUser(user);
setLoading(false);
} else {
setUser(null);
setIsClient(false);
setIsMaster(false);
setClientName(null);
setUserEmail(null);
setLoading(false);
setIsPremium(false);
}
});
return fetchedUser;
};
useQuery("fetchClaimsData", fetchClaims );
The Context:
I want to know how to get rid of this error:
Warning: Can't perform a React state update on an unmounted component.
I think I know exactly why this error shows up:
I have a Signin Route. I have a litte piece of code in the useEffect that does this:
if (!auth.isEmpty && auth.isLoaded) {
history.push("/");
}
So when someone goes to mypage/signin and is already signed in, he gets redirected to Homepage. This works fine BUT:
The Problem:
When he is not signed in I have a little Signin Function:
const signin = async (e: React.MouseEvent) => {
setIsLoading(true);
e.preventDefault();
try {
const user = await firebase.login({ email, password });
setIsLoading(false);
if (user) {
history.push("/");
}
} catch (error) {
setIsLoading(false);
setError(error.message);
}
};
So when the users hits enter, he gets redirected to home when there is no error. It works fine but I get this error in the console, because I set the state and the the snippet in useEffect routes me to /Home, but the promise is not yet completed from firebase. And when it's finished it tries to set state, but component already unmounted.
What have I tried
I added a isMounted hook and changed my signin function to look like this:
const signin = async (e: React.MouseEvent) => {
e.preventDefault();
if (isMounted) {
setIsLoading(true);
try {
const user = await firebase.login({ email, password });
setIsLoading(false);
if (user) {
history.push("/");
}
} catch (error) {
setIsLoading(false);
setError(error.message);
}
}
};
But still the same error on route change.
Additional Info
Don't get confused about these 2 loading states auth.isLoaded (from react-redux-firebase) and isLoading (my own state). Basically why I did it this way is, because when someone is already logged in and then goes to /signin he sees the login form for a tiny moment, because firebase doesn't know yet if user is authenticated, so I handled it like this, so the user definetily sees a spinner and then gets redirected if already logged in.
How to solve this little problem?
You can use React hooks for this. The useEffect return method is called when compoment is unmonuted from screen. This is like compomentdidunmount in class based react.
declare global variable _isMounted to false. When useEffect is called, it changes to true and components are on screen.
If component are unmounted, then return method from useEffect is called and _isMounted is set to false;
while updating the state, you can check using _isMounted variable that is component is mounted or not.
var _isMounted = false;
const fetchuser = () => {
if(_isMounted)
{
// code
}
}
useEffect(() => {
_isMounted = true;
// your code;
return()
{
_isMounted = false;
console.log("Component Unmounted");
}
},[])
if you redirected after login, you dont have to change loading state. Just remove setIsLoading(false)
const user = await firebase.login({ email, password });
if (user) {
history.push("/");
}
I have a component updating the user's profile pic:
const Updater = () => {
const updateProfilePic = async (photoURL) => {
await auth.currentUser.updateProfile({ 'photoURL': photoURL });
}
}
I have a second component detecting changes in the user's state:
const StateChangesDetector = () => {
auth.onAuthStateChanged( user => {
if(user)
console.log('User changed state', JSON.stringify(user));
});
}
The problem is that auth.onAuthStateChanged() is not triggering after the execution of updateProfile(). Thus, I'm getting the old user's state and the old profile picture.
How can I force to trigger auth.onAuthStateChanged() after updating the user's profile picture?
Try the reload() function on the currentUser object to get updated information. Note that it's asynchronous and returns a promise. If it doesn't trigger the listener (as it's not really a "state" change, just a refresh of data), I suspect you might have to access the firebase.auth().currentUser again after the returned promise resolves to see new data.
I want to call useQuery whenever I need it,
but useQuery can not inside the function.
My trying code is:
export const TestComponent = () => {
...
const { data, loading, error } = useQuery(gql(GET_USER_LIST), {
variables: {
data: {
page: changePage,
pageSize: 10,
},
},
})
...
...
const onSaveInformation = async () => {
try {
await updateInformation({...})
// I want to call useQuery once again.
} catch (e) {
return e
}
}
...
How do I call useQuery multiple times?
Can I call it whenever I want?
I have looked for several sites, but I could not find a solutions.
From apollo docs
When React mounts and renders a component that calls the useQuery hook, Apollo Client automatically executes the specified query. But what if you want to execute a query in response to a different event, such as a user clicking a button?
The useLazyQuery hook is perfect for executing queries in response to
events other than component rendering
I suggest useLazyQuery. In simple terms, useQuery will run when your component get's rendered, you can use skip option to skip the initial run. And there are some ways to refetch/fetch more data whenever you want. Or you can stick with useLazyQuery
E.g If you want to fetch data when only user clicks on a button or scrolls to the bottom, then you can use useLazyQuery hook.
useQuery is a declarative React Hook. It is not meant to be called in the sense of a classic function to receive data. First, make sure to understand React Hooks or simply not use them for now (90% of questions on Stackoverflow happen because people try to learn too many things at once). The Apollo documentation is very good for the official react-apollo package, which uses render props. This works just as well and once you have understood Apollo Client and Hooks you can go for a little refactor. So the answers to your questions:
How do I call useQuery multiple times?
You don't call it multiple times. The component will automatically rerender when the query result is available or gets updated.
Can I call it whenever I want?
No, hooks can only be called on the top level. Instead, the data is available in your function from the upper scope (closure).
Your updateInformation should probably be a mutation that updates the application's cache, which again triggers a rerender of the React component because it is "subscribed" to the query. In most cases, the update happens fully automatically because Apollo will identify entities by a combination of __typename and id. Here's some pseudocode that illustrates how mutations work together with mutations:
const GET_USER_LIST = gql`
query GetUserList {
users {
id
name
}
}
`;
const UPDATE_USER = gql`
mutation UpdateUser($id: ID!, $name: String!) {
updateUser(id: $id, update: { name: $name }) {
success
user {
id
name
}
}
}
`;
const UserListComponen = (props) => {
const { data, loading, error } = useQuery(GET_USER_LIST);
const [updateUser] = useMutation(UPDATE_USER);
const onSaveInformation = (id, name) => updateUser({ variables: { id, name });
return (
// ... use data.users and onSaveInformation in your JSX
);
}
Now if the name of a user changes via the mutation Apollo will automatically update the cache und trigger a rerender of the component. Then the component will automatically display the new data. Welcome to the power of GraphQL!
There's answering mentioning how useQuery should be used, and also suggestions to use useLazyQuery. I think the key takeaway is understanding the use cases for useQuery vs useLazyQuery, which you can read in the documentation. I'll try to explain it below from my perspective.
useQuery is "declarative" much like the rest of React, especially component rendering. This means you should expect useQuery to be called every render when state or props change. So in English, it's like, "Hey React, when things change, this is what I want you to query".
for useLazyQuery, this line in the documentation is key: "The useLazyQuery hook is perfect for executing queries in response to events other than component rendering". In more general programming speak, it's "imperative". This gives you the power to call the query however you want, whether it's in response to state/prop changes (i.e. with useEffect) or event handlers like button clicks. In English, it's like, "Hey React, this is how I want to query for the data".
You can use fetchMore() returned from useQuery, which is primarily meant for pagination.
const { loading, client, fetchMore } = useQuery(GET_USER_LIST);
const submit = async () => {
// Perform save operation
const userResp = await fetchMore({
variables: {
// Pass any args here
},
updateQuery(){
}
});
console.log(userResp.data)
};
Read more here: fetchMore
You could also use useLazyQuery, however it'll give you a function that returns void and the data is returned outside your function.
const [getUser, { loading, client, data }] = useLazyQuery(GET_USER_LIST);
const submit = async () => {
const userResp = await getUser({
variables: {
// Pass your args here
},
updateQuery() {},
});
console.log({ userResp }); // undefined
};
Read more here: useLazyQuery
You can create a reusable fetch function as shown below:
// Create query
const query = `
query GetUserList ($data: UserDataType){
getUserList(data: $data){
uid,
first_name
}
}
`;
// Component
export const TestComponent (props) {
const onSaveInformation = async () => {
// I want to call useQuery once again.
const getUsers = await fetchUserList();
}
// This is the reusable fetch function.
const fetchUserList = async () => {
// Update the URL to your Graphql Endpoint.
return await fetch('http://localhost:8080/api/graphql?', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
query,
variables: {
data: {
page: changePage,
pageSize: 10,
},
},
})
}).then(
response => { return response.json(); }
).catch(
error => console.log(error) // Handle the error response object
);
}
return (
<h1>Test Component</h1>
);
}
Here's an alternative that worked for me:
const { refetch } = useQuery(GET_USER_LIST, {
variables: {
data: {
page: changePage,
pageSize: 10,
},
},
}
);
const onSaveInformation = async () => {
try {
await updateInformation({...});
const res = await refetch({ variables: { ... }});
console.log(res);
} catch (e) {
return e;
}
}
And here's a similar answer for a similar question.
Please use
const { loading, data, refetch } = useQuery(Query_Data)
and call it when you need it i.e
refetch()
I have a login form which needs to re-direct a user to a landing page if the user's email exists in the database.
I have a class called "FormToLogin" with a method called login. In the login method, I dispatch data and this.props.history to an action called loginAct.
Container:
class FormToLogin extends Component {
login = fullForm => {
const { dispatch } = this.props;
const data = {[user]: {...fullForm}}
return dispatch(login(data, this.props.history)) <-- apparently, passing history to an action is not good)
}
}
As you can see, I call the action by passing Data (which will include the email address entered by the user) and the history because I want to make a .push('/new_url') if the email exists in the database.
Action:
export const login = (data, history, dispatch) => {
return api.post(url, data)
.then(({ status, h, data }) => {
// whatever if it returns 200
}
.catch(({ response }) => {
dispatch(loginFail());
const status = (response || {}).status;
if (status === 401 && hasError(error_user, response.data)) {
history.push('/new_url') // This is INCORRECT
?? -- what do I need here -- ??
}
})
}
I have been told that it's bad practice to pass Route history to an Action.
So, history.push() shouldn't happen here.
I've been suggested to add a catch to a container level ("FormToLogin").
So, I've tried to create a catch in the Container(FormToLogin) when I call the Action with dispatch(login(data)), but it doesn't work. Also, var status doesn't exist in the container.
BEFORE: return dispatch(login(data, this.props.history))
AFTER: .catch(e => {
if (status === 401 && hasError(
error_user,
e.response.data
)) {
history.push('/new_url);
} throw e; })
What do I need to add or change?
Two ways to solve this issue.
1) Accessing history object inside Action creator without explicitly passing.
// create history object
history.js
import createHistory from 'history/createHashHistory'
export default createHistory()
action.js
import history from './history'
history.push('/new_url') // use it wherever you want.
2) If you don't want it inside action then handle that inside formLogin.
When dispatching dispatch(loginFail());, inside loginFail function set state of email_address. You could get that state using connect function inside FormToLogin due to react-redux library using props.
Inside render function you could write.
if (this.props.isEmailAddress) { history.push('/new_url') }