In react navigation (I could do this in App.ts too) I fire off the authentication like so:
export default function Navigation() {
authenticateUser();
...
}
export default function authenticateUser() {
const setLoadingUser = useStore((state) => state.setLoadingUser);
firebase.auth().onAuthStateChanged(async (authenticatedUser) => {
console.log('AuthenticateUser', authenticatedUser);
setLoadingUser(false);
if (authenticatedUser) {
useAuthenticate(authenticatedUser);
} else {
console.log('No user');
setLoadingUser(false);
}
});
...
}
And for the sake of simplicity, I will just print the user for now:
import { useEffect } from 'react';
export const useAuthenticate = (authenticatedUser) => {
useEffect(() => {
console.log('authenticatedUser', authenticatedUser);
}, [authenticatedUser]);
return true;
};
I believe that because I'm calling useAuthenticate inside the async firebase onAuthStateChanged function, React is throwing [Unhandled promise rejection: Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:]
How do I handle this?
This should be:
export default function authenticateUser() {
const {setAuthenticated} = useAuthenticate();
const setLoadingUser = useStore((state) => state.setLoadingUser);
firebase.auth().onAuthStateChanged(async (authenticatedUser) => {
console.log('AuthenticateUser', authenticatedUser);
setLoadingUser(false);
if (authenticatedUser) {
setAuthenticated(authenticatedUser);
} else {
console.log('No user');
setLoadingUser(false);
}
});
...
}
import { useEffect, useState } from 'react';
export const useAuthenticate = () => {
const [authenticated, setAuthenticated] = useState(false);
useEffect(() => {
console.log('authenticatedUser', authenticated);
}, [authenticated]);
return {authenticated, setAuthenticated};
};
Related
Trying to test a status hook that uses a promise that is not getting updated by my test.
screens.OnStart() should trigger setStatus with the value the promise returns.
When I log status it never changes.
import { useEffect, useState } from 'react'
import screens from '#utils/screen'
const useStatus = () => {
const [status, setStatus] = useState()
useEffect(() => {
const listener = screens.OnStart(
"HAPPEN",
({ status }) =>
setStatus(status)
)
return () => {
screens.removeListener("HAPPEN", listener)
}
}, [])
return {
status,
}
}
export default useStatus
Test
import React from 'react'
import { act, renderHook } from '#testing-library/react-hooks'
import useStatus from '#hooks/useStatus'
const mockedOnStart = jest.fn().mockImplementation((event, callback) => callback)
jest.mock('#utils/screens', () => ({
...jest.requireActual('#utils/screens'),
default: {
OnStart: () => mockedOnStart(),
},
__esModule: true,
}))
describe('useStatus', () => {
test('Renders', async () => {
mockedOnStart.mockReturnValueOnce(2)
const { result } = renderHook(() => useStatus())
await act(async () => {
console.log('result = ', result.current.status)
})
})
})
Using react native with expo-location for a mobile app, I would like to unsubscribe from Location.watchPositionAsync which returns a promise with a remove() method to unsubscribe.
I call the function within a useEffect hooks, but i don't know how to correctly return a cleanup function with the watchPositionAsync promise resolved.
Any suggestions?
import { useState, useEffect } from "react";
import { Text, View } from "react-native";
import * as Location from "expo-location";
export const GpsComponent = function () {
const [location, setLocation] = useState(null);
useEffect(() => {
const positionSubscription = async () => {
const positionSubscribe = await Location.watchPositionAsync(
{ accuracy: Location.LocationAccuracy.BestForNavigation },
(newLocation) => {
setLocation(newLocation);
}
);
return positionSubscribe;
};
/*return () => {
positionSubscription.remove();
console.log("Unsubscribed from WatchPositionAsync");
};*/
}, [setLocation]);
return (
<View>
<Text>{JSON.stringify(location)}</Text>
</View>
);
};
This will create the watchPositionAsync subscription and pass the correct remove function as the cleanup of the useEffect. A dummy subscription is created initially with a nop remove function.
useEffect(() => {
// nop subscription. in case not successful
let subscription = { remove: () => {} }
// subscribe async function
const subscribe = async () => {
return await Location.watchPositionAsync(
{ accuracy: Location.LocationAccuracy.Highest },
(newLocation) => {
setLocation(newLocation)
}
)
}
// return subscription promise
subscribe()
.then(result => subscription = result)
.catch(err => console.warn(err))
// return remove function for cleanup
return subscription.remove
}, [])
I finally found a way to unsubscribe to watchPositionAsync using useRef
import { useState, useEffect, useRef } from "react";
import { Text, View } from "react-native";
import * as Location from "expo-location";
export const GpsComponent = function () {
const [location, setLocation] = useState(null);
const unsubscribe = useRef(() => undefined);
useEffect(() => {
const subscribe= async () => {
const positionSubscription = await Location.watchPositionAsync(
{ accuracy: Location.LocationAccuracy.BestForNavigation },
(newLocation) => {
setLocation(newLocation);
}
);
unsubscribe.current=()=>{positionSubscription?.remove()}
};
return ()=>{unsubscribe.current()}
}, []);
return (
<View>
<Text>{JSON.stringify(location)}</Text>
</View>
);
};
It 's also possible to use an object and modify a property after the async function's promise is resolved.
I have this code that just changes the loading status if url changes.
_app.tsx
useEffect(() => {
const handleRouteStart = (url, { shallow }) => {
console.log(url);
setLoading((p) => !p);
};
const handleRouteComplete = (url, { shallow }) => {
setLoading(false);
};
const handleRouteError = (url, { shallow }) => {
setLoading(false);
};
router.events.on("routeChangeStart", handleRouteStart);
router.events.on("routeChangeComplete", handleRouteComplete);
router.events.on("routeChangeError", handleRouteError);
return () => {
router.events.off("routeChangeStart", handleRouteStart);
router.events.off("routeChangeComplete", handleRouteComplete);
router.events.off("routeChangeError", handleRouteError);
};
}, []);
pages/index.tsx
I use this
router.push("/map"); and it doesn't work...I checked and found out that it is because of routeChangeStart, etc these events
How can I solve this?
index.tsx
import Link from "next/link";
import React, { useEffect } from "react";
import { useRouter } from "next/router";
function Home(props) {
const router = useRouter();
if (cookies(props).firstLaunch === "false") {
router.push("/map"); <=== doesn't run
return <LoadingPage title={"title"} />; <=== it executes correctly
}
return (
...
);
}
When my user logs out, I want to remove all user data from the app, but I'm having trouble implementing this.
I have a custom useUserData() hook that gets the user's data. getUser() is a callable cloud function. This is my code so far.
import { useEffect, useState } from "react"
import { useQuery, useQueryClient } from "react-query"
import { getUser } from "Services/firebase/functions"
import firebase from "firebase/app"
export default function useUserData(){
const [ enabled, setEnabled] = useState(false)
const queryClient = useQueryClient()
useEffect(_ => {
const unsubscribe = firebase.auth().onAuthStateChanged(user => {
setEnabled(Boolean(user))
if (!user){
// remove data
}
else queryClient.invalidateQueries("user", { refetchActive: true, refetchInactive: true })
})
return unsubscribe()
}, [])
return useQuery(
"user",
() => getUser().then(res => res.data),
{
enabled
}
)
}
Edit:
It seemed that I was handling my effect cleanup wrong. This seems to be working.
import { useEffect, useState } from "react"
import { useQuery, useQueryClient } from "react-query"
import { getUser } from "Services/firebase/functions"
import firebase from "firebase/app"
export default function useUserData(){
const [ enabled, setEnabled] = useState(false)
const queryClient = useQueryClient()
useEffect(_ => {
const unsubscribe = firebase.auth().onAuthStateChanged(user => {
setEnabled(Boolean(user))
if (!user) {
queryClient.removeQueries("user")
}
})
return _ => unsubscribe()
}, [])
return useQuery(
"user",
() => getUser().then(res => res.data),
{
enabled
}
)
}
Weirdly enough, the query still fetches once after logging out, when the query should already be disabled.
queryClient.removeQueries("user")
will remove all user related queries. It's a good thing to do on logout. You can clear everything by calling removeQueries without parameters.
Here's my current implementation which seems to work fine.
import { useEffect, useState } from "react"
import { useQuery, useQueryClient } from "react-query";
import firebase from "firebase/app"
export default function useAuthenticatedQuery(key, func, options){
const [ enabled, setEnabled] = useState(false)
const queryClient = useQueryClient()
useEffect(_ => {
const unsubscribe = firebase.auth().onAuthStateChanged(user => {
setEnabled(Boolean(user))
if (!user){
queryClient.setQueryData(key, _ => undefined)
queryClient.removeQueries(key, _ => undefined)
}else
queryClient.invalidateQueries(key, { refetchActive: true, refetchInactive: true })
})
return _ => unsubscribe()
// eslint-disable-next-line
}, [])
return useQuery(
key,
func,
{
...options,
enabled
}
)
}
I use it just like the regular useQuery hook:
import useAuthenticatedQuery from "Hooks/useAuthenticatedQuery"
export default function useUserData(){
return useAuthenticatedQuery(
"user",
() => getUser().then(res => res.data)
)
}
I have a React Native App,
Here i use mobx ("mobx-react": "^6.1.8") and react hooks.
i get the error:
Invalid hook call. Hooks can only be called inside of the body of a function component
Stores index.js
import { useContext } from "react";
import UserStore from "./UserStore";
import SettingsStore from "./SettingsStore";
const useStore = () => {
return {
UserStore: useContext(UserStore),
SettingsStore: useContext(SettingsStore),
};
};
export default useStore;
helper.js OLD
import React from "react";
import useStores from "../stores";
export const useLoadAsyncProfileDependencies = userID => {
const { ExamsStore, UserStore, CTAStore, AnswersStore } = useStores();
const [user, setUser] = useState({});
const [ctas, setCtas] = useState([]);
const [answers, setAnswers] = useState([]);
useEffect(() => {
if (userID) {
(async () => {
const user = await UserStore.initUser();
UserStore.user = user;
setUser(user);
})();
(async () => {
const ctas = await CTAStore.getAllCTAS(userID);
CTAStore.ctas = ctas;
setCtas(ctas);
})();
(async () => {
const answers = await AnswersStore.getAllAnswers(userID);
UserStore.user.answers = answers.items;
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
setAnswers(answers.items);
})();
}
}, [userID]);
};
Screen
import React, { useEffect, useState, useRef } from "react";
import {
View,
Dimensions,
SafeAreaView,
ScrollView,
StyleSheet
} from "react-native";
import {
widthPercentageToDP as wp,
heightPercentageToDP as hp
} from "react-native-responsive-screen";
import { observer } from "mobx-react";
import useStores from "../../stores";
import { useLoadAsyncProfileDependencies } from "../../helper/app";
const windowWidth = Dimensions.get("window").width;
export default observer(({ navigation }) => {
const {
UserStore,
ExamsStore,
CTAStore,
InternetConnectionStore
} = useStores();
const scrollViewRef = useRef();
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
if (InternetConnectionStore.isOffline) {
return;
}
Tracking.trackEvent("opensScreen", { name: "Challenges" });
useLoadAsyncProfileDependencies(UserStore.userID);
}, []);
React.useEffect(() => {
const unsubscribe = navigation.addListener("focus", () => {
CTAStore.popBadget(BadgetNames.ChallengesTab);
});
return unsubscribe;
}, [navigation]);
async function refresh() {
const user = await UserStore.initUser(); //wird das gebarucht?
useLoadAsyncProfileDependencies(UserStore.userID);
if (user) {
InternetConnectionStore.isOffline = false;
}
}
const name = UserStore.name;
return (
<SafeAreaView style={styles.container} forceInset={{ top: "always" }}>
</SafeAreaView>
);
});
so now, when i call the useLoadAsyncProfileDependencies function, i get this error.
The Problem is that i call useStores in helper.js
so when i pass the Stores from the Screen to the helper it is working.
export const loadAsyncProfileDependencies = async ({
ExamsStore,
UserStore,
CTAStore,
AnswersStore
}) => {
const userID = UserStore.userID;
if (userID) {
UserStore.initUser().then(user => {
UserStore.user = user;
});
CTAStore.getAllCTAS(userID).then(ctas => {
console.log("test", ctas);
CTAStore.ctas = ctas;
});
AnswersStore.getAllAnswers(userID).then(answers => {
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
});
}
};
Is there a better way? instead passing the Stores.
So that i can use this function in functions?
As the error says, you can only use hooks inside the root of a functional component, and your useLoadAsyncProfileDependencies is technically a custom hook so you cant use it inside a class component.
https://reactjs.org/warnings/invalid-hook-call-warning.html
EDIT: Well after showing the code for app.js, as mentioned, hook calls can only be done top level from a function component or the root of a custom hook. You need to rewire your code to use custom hooks.
SEE THIS: https://reactjs.org/docs/hooks-rules.html
You should return the value for _handleAppStateChange so your useEffect's the value as a depdendency in your root component would work properly as intended which is should run only if value has changed. You also need to rewrite that as a custom hook so you can call hooks inside.
doTasksEveryTimeWhenAppWillOpenFromBackgorund and doTasksEveryTimeWhenAppGoesToBackgorund should also be written as a custom hook so you can call useLoadAsyncProfileDependencies inside.
write those hooks in a functional way so you are isolating specific tasks and chain hooks as you wish without violiating the rules of hooks. Something like this:
const useGetMyData = (params) => {
const [data, setData] = useState()
useEffect(() => {
(async () => {
const apiData = await myApiCall(params)
setData(apiData)
})()
}, [params])
return data
}
Then you can call that custom hook as you wish without violation like:
const useShouldGetData = (should, params) => {
if (should) {
return useGetMyData()
}
return null
}
const myApp = () => {
const myData = useShouldGetData(true, {id: 1})
return (
<div>
{JSON.stringify(myData)}
</div>
)
}