How to reset recaptcha on Success for signInWithPhoneNumber() - javascript

I am using Firebase.auth()signInWithPhoneNumber(number, appVerifier)
Everything is working as expected however I am trying to resolve the issue below:
Here is my implementation:
useEffect(() => {
window.recaptchaVerifier = new app.auth.RecaptchaVerifier("sendBtn", {
size: "invisible",
callback: function () {
onSend();
},
});
});
const onSend = (value) => {
const appVerifier = window.recaptchaVerifier;
const setMobile = "valid mobile..";
firebase
.auth()
.signInWithPhoneNumber(setMobile, appVerifier)
.then(function (confirmationResult) {
appVerifier.reset()
console.log(confirmationResult)
})
.catch(function (error) {
appVerifier.reset()
console.log(error);
});
};
How can I correctly handle Recaptcha? Without it being rendered multiple times. I'm looking to destroy it on Recaptcha on success, I have gone through the documentation here but clear() or reset() does not seem to work

You can provide a empty array of dependencies to useEffect to trigger only after initial render, more details in this Stack Overflow Answer.
Additionally it may be a good idea to add an if check to see if window.recaptchaVerifier is set (in case you have component using recaptcha anywhere else on your page), before trying to initialize a new RecaptchaVerifier.
useEffect(() => {
if (!window.recaptchaVerifier) {
window.recaptchaVerifier = new app.auth.RecaptchaVerifier('sendBtn', {
size: 'invisible',
callback: function () {
onSend();
}
});
}
}, []);

You have to provide the dependencies of your useEffect otherwise it will be executed each time the component render.
useEffect(() => {
// recaptcha
}, [])

Related

custom hook memory leak [duplicate]

When fetching data I'm getting: Can't perform a React state update on an unmounted component. The app still works, but react is suggesting I might be causing a memory leak.
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."
Why do I keep getting this warning?
I tried researching these solutions:
https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
https://developer.mozilla.org/en-US/docs/Web/API/AbortController
but this still was giving me the warning.
const ArtistProfile = props => {
const [artistData, setArtistData] = useState(null)
const token = props.spotifyAPI.user_token
const fetchData = () => {
const id = window.location.pathname.split("/").pop()
console.log(id)
props.spotifyAPI.getArtistProfile(id, ["album"], "US", 10)
.then(data => {setArtistData(data)})
}
useEffect(() => {
fetchData()
return () => { props.spotifyAPI.cancelRequest() }
}, [])
return (
<ArtistProfileContainer>
<AlbumContainer>
{artistData ? artistData.artistAlbums.items.map(album => {
return (
<AlbumTag
image={album.images[0].url}
name={album.name}
artists={album.artists}
key={album.id}
/>
)
})
: null}
</AlbumContainer>
</ArtistProfileContainer>
)
}
Edit:
In my api file I added an AbortController() and used a signal so I can cancel a request.
export function spotifyAPI() {
const controller = new AbortController()
const signal = controller.signal
// code ...
this.getArtist = (id) => {
return (
fetch(
`https://api.spotify.com/v1/artists/${id}`, {
headers: {"Authorization": "Bearer " + this.user_token}
}, {signal})
.then(response => {
return checkServerStat(response.status, response.json())
})
)
}
// code ...
// this is my cancel method
this.cancelRequest = () => controller.abort()
}
My spotify.getArtistProfile() looks like this
this.getArtistProfile = (id,includeGroups,market,limit,offset) => {
return Promise.all([
this.getArtist(id),
this.getArtistAlbums(id,includeGroups,market,limit,offset),
this.getArtistTopTracks(id,market)
])
.then(response => {
return ({
artist: response[0],
artistAlbums: response[1],
artistTopTracks: response[2]
})
})
}
but because my signal is used for individual api calls that are resolved in a Promise.all I can't abort() that promise so I will always be setting the state.
For me, clean the state in the unmount of the component helped.
const [state, setState] = useState({});
useEffect(() => {
myFunction();
return () => {
setState({}); // This worked for me
};
}, []);
const myFunction = () => {
setState({
name: 'Jhon',
surname: 'Doe',
})
}
Sharing the AbortController between the fetch() requests is the right approach.
When any of the Promises are aborted, Promise.all() will reject with AbortError:
function Component(props) {
const [fetched, setFetched] = React.useState(false);
React.useEffect(() => {
const ac = new AbortController();
Promise.all([
fetch('http://placekitten.com/1000/1000', {signal: ac.signal}),
fetch('http://placekitten.com/2000/2000', {signal: ac.signal})
]).then(() => setFetched(true))
.catch(ex => console.error(ex));
return () => ac.abort(); // Abort both fetches on unmount
}, []);
return fetched;
}
const main = document.querySelector('main');
ReactDOM.render(React.createElement(Component), main);
setTimeout(() => ReactDOM.unmountComponentAtNode(main), 1); // Unmount after 1ms
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.development.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.development.js"></script>
<main></main>
For example, you have some component that does some asynchronous actions, then writes the result to state and displays the state content on a page:
export default function MyComponent() {
const [loading, setLoading] = useState(false);
const [someData, setSomeData] = useState({});
// ...
useEffect( () => {
(async () => {
setLoading(true);
someResponse = await doVeryLongRequest(); // it takes some time
// When request is finished:
setSomeData(someResponse.data); // (1) write data to state
setLoading(false); // (2) write some value to state
})();
}, []);
return (
<div className={loading ? "loading" : ""}>
{someData}
<Link to="SOME_LOCAL_LINK">Go away from here!</Link>
</div>
);
}
Let's say that user clicks some link when doVeryLongRequest() still executes. MyComponent is unmounted but the request is still alive and when it gets a response it tries to set state in lines (1) and (2) and tries to change the appropriate nodes in HTML. We'll get an error from subject.
We can fix it by checking whether compponent is still mounted or not. Let's create a componentMounted ref (line (3) below) and set it true. When component is unmounted we'll set it to false (line (4) below). And let's check the componentMounted variable every time we try to set state (line (5) below).
The code with fixes:
export default function MyComponent() {
const [loading, setLoading] = useState(false);
const [someData, setSomeData] = useState({});
const componentMounted = useRef(true); // (3) component is mounted
// ...
useEffect( () => {
(async () => {
setLoading(true);
someResponse = await doVeryLongRequest(); // it takes some time
// When request is finished:
if (componentMounted.current){ // (5) is component still mounted?
setSomeData(someResponse.data); // (1) write data to state
setLoading(false); // (2) write some value to state
}
return () => { // This code runs when component is unmounted
componentMounted.current = false; // (4) set it to false when we leave the page
}
})();
}, []);
return (
<div className={loading ? "loading" : ""}>
{someData}
<Link to="SOME_LOCAL_LINK">Go away from here!</Link>
</div>
);
}
Why do I keep getting this warning?
The intention of this warning is to help you prevent memory leaks in your application. If the component updates it's state after it has been unmounted from the DOM, this is an indication that there could be a memory leak, but it is an indication with a lot of false positives.
How do I know if I have a memory leak?
You have a memory leak if an object that lives longer than your component holds a reference to it, either directly or indirectly. This usually happens when you subscribe to events or changes of some kind without unsubscribing when your component unmounts from the DOM.
It typically looks like this:
useEffect(() => {
function handleChange() {
setState(store.getState())
}
// "store" lives longer than the component,
// and will hold a reference to the handleChange function.
// Preventing the component to be garbage collected after
// unmount.
store.subscribe(handleChange)
// Uncomment the line below to avoid memory leak in your component
// return () => store.unsubscribe(handleChange)
}, [])
Where store is an object that lives further up the React tree (possibly in a context provider), or in global/module scope. Another example is subscribing to events:
useEffect(() => {
function handleScroll() {
setState(window.scrollY)
}
// document is an object in global scope, and will hold a reference
// to the handleScroll function, preventing garbage collection
document.addEventListener('scroll', handleScroll)
// Uncomment the line below to avoid memory leak in your component
// return () => document.removeEventListener(handleScroll)
}, [])
Another example worth remembering is the web API setInterval, which can also cause memory leak if you forget to call clearInterval when unmounting.
But that is not what I am doing, why should I care about this warning?
React's strategy to warn whenever state updates happen after your component has unmounted creates a lot of false positives. The most common I've seen is by setting state after an asynchronous network request:
async function handleSubmit() {
setPending(true)
await post('/someapi') // component might unmount while we're waiting
setPending(false)
}
You could technically argue that this also is a memory leak, since the component isn't released immediately after it is no longer needed. If your "post" takes a long time to complete, then it will take a long time to for the memory to be released. However, this is not something you should worry about, because it will be garbage collected eventually. In these cases, you could simply ignore the warning.
But it is so annoying to see the warning, how do I remove it?
There are a lot of blogs and answers on stackoverflow suggesting to keep track of the mounted state of your component and wrap your state updates in an if-statement:
let isMountedRef = useRef(false)
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
}
}, [])
async function handleSubmit() {
setPending(true)
await post('/someapi')
if (!isMountedRef.current) {
setPending(false)
}
}
This is not an recommended approach! Not only does it make the code less readable and adds runtime overhead, but it might also might not work well with future features of React. It also does nothing at all about the "memory leak", the component will still live just as long as without that extra code.
The recommended way to deal with this is to either cancel the asynchronous function (with for instance the AbortController API), or to ignore it.
In fact, React dev team recognises the fact that avoiding false positives is too difficult, and has removed the warning in v18 of React.
You can try this set a state like this and check if your component mounted or not. This way you are sure that if your component is unmounted you are not trying to fetch something.
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
return () => setDidMount(false);
}, [])
if(!didMount) {
return null;
}
return (
<ArtistProfileContainer>
<AlbumContainer>
{artistData ? artistData.artistAlbums.items.map(album => {
return (
<AlbumTag
image={album.images[0].url}
name={album.name}
artists={album.artists}
key={album.id}
/>
)
})
: null}
</AlbumContainer>
</ArtistProfileContainer>
)
Hope this will help you.
I had a similar issue with a scroll to top and #CalosVallejo answer solved it :) Thank you so much!!
const ScrollToTop = () => {
const [showScroll, setShowScroll] = useState();
//------------------ solution
useEffect(() => {
checkScrollTop();
return () => {
setShowScroll({}); // This worked for me
};
}, []);
//----------------- solution
const checkScrollTop = () => {
setShowScroll(true);
};
const scrollTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" });
};
window.addEventListener("scroll", checkScrollTop);
return (
<React.Fragment>
<div className="back-to-top">
<h1
className="scrollTop"
onClick={scrollTop}
style={{ display: showScroll }}
>
{" "}
Back to top <span>⟶ </span>
</h1>
</div>
</React.Fragment>
);
};
I have getting same warning, This solution Worked for me ->
useEffect(() => {
const unsubscribe = fetchData(); //subscribe
return unsubscribe; //unsubscribe
}, []);
if you have more then one fetch function then
const getData = () => {
fetch1();
fetch2();
fetch3();
}
useEffect(() => {
const unsubscribe = getData(); //subscribe
return unsubscribe; //unsubscribe
}, []);
This error occurs when u perform state update on current component after navigating to other component:
for example
axios
.post(API.BASE_URI + API.LOGIN, { email: username, password: password })
.then((res) => {
if (res.status === 200) {
dispatch(login(res.data.data)); // line#5 logging user in
setSigningIn(false); // line#6 updating some state
} else {
setSigningIn(false);
ToastAndroid.show(
"Email or Password is not correct!",
ToastAndroid.LONG
);
}
})
In above case on line#5 I'm dispatching login action which in return navigates user to the dashboard and hence login screen now gets unmounted.
Now when React Native reaches as line#6 and see there is state being updated, it yells out loud that how do I do this, the login component is there no more.
Solution:
axios
.post(API.BASE_URI + API.LOGIN, { email: username, password: password })
.then((res) => {
if (res.status === 200) {
setSigningIn(false); // line#6 updating some state -- moved this line up
dispatch(login(res.data.data)); // line#5 logging user in
} else {
setSigningIn(false);
ToastAndroid.show(
"Email or Password is not correct!",
ToastAndroid.LONG
);
}
})
Just move react state update above, move line 6 up the line 5.
Now state is being updated before navigating the user away. WIN WIN
there are many answers but I thought I could demonstrate more simply how the abort works (at least how it fixed the issue for me):
useEffect(() => {
// get abortion variables
let abortController = new AbortController();
let aborted = abortController.signal.aborted; // true || false
async function fetchResults() {
let response = await fetch(`[WEBSITE LINK]`);
let data = await response.json();
aborted = abortController.signal.aborted; // before 'if' statement check again if aborted
if (aborted === false) {
// All your 'set states' inside this kind of 'if' statement
setState(data);
}
}
fetchResults();
return () => {
abortController.abort();
};
}, [])
Other Methods:
https://medium.com/wesionary-team/how-to-fix-memory-leak-issue-in-react-js-using-hook-a5ecbf9becf8
If the user navigates away, or something else causes the component to get destroyed before the async call comes back and tries to setState on it, it will cause the error. It's generally harmless if it is, indeed, a late-finish async call. There's a couple of ways to silence the error.
If you're implementing a hook like useAsync you can declare your useStates with let instead of const, and, in the destructor returned by useEffect, set the setState function(s) to a no-op function.
export function useAsync<T, F extends IUseAsyncGettor<T>>(gettor: F, ...rest: Parameters<F>): IUseAsync<T> {
let [parameters, setParameters] = useState(rest);
if (parameters !== rest && parameters.some((_, i) => parameters[i] !== rest[i]))
setParameters(rest);
const refresh: () => void = useCallback(() => {
const promise: Promise<T | void> = gettor
.apply(null, parameters)
.then(value => setTuple([value, { isLoading: false, promise, refresh, error: undefined }]))
.catch(error => setTuple([undefined, { isLoading: false, promise, refresh, error }]));
setTuple([undefined, { isLoading: true, promise, refresh, error: undefined }]);
return promise;
}, [gettor, parameters]);
useEffect(() => {
refresh();
// and for when async finishes after user navs away //////////
return () => { setTuple = setParameters = (() => undefined) }
}, [refresh]);
let [tuple, setTuple] = useState<IUseAsync<T>>([undefined, { isLoading: true, refresh, promise: Promise.resolve() }]);
return tuple;
}
That won't work well in a component, though. There, you can wrap useState in a function which tracks mounted/unmounted, and wraps the returned setState function with the if-check.
export const MyComponent = () => {
const [numPendingPromises, setNumPendingPromises] = useUnlessUnmounted(useState(0));
// ..etc.
// imported from elsewhere ////
export function useUnlessUnmounted<T>(useStateTuple: [val: T, setVal: Dispatch<SetStateAction<T>>]): [T, Dispatch<SetStateAction<T>>] {
const [val, setVal] = useStateTuple;
const [isMounted, setIsMounted] = useState(true);
useEffect(() => () => setIsMounted(false), []);
return [val, newVal => (isMounted ? setVal(newVal) : () => void 0)];
}
You could then create a useStateAsync hook to streamline a bit.
export function useStateAsync<T>(initialState: T | (() => T)): [T, Dispatch<SetStateAction<T>>] {
return useUnlessUnmounted(useState(initialState));
}
Try to add the dependencies in useEffect:
useEffect(() => {
fetchData()
return () => { props.spotifyAPI.cancelRequest() }
}, [fetchData, props.spotifyAPI])
Usually this problem occurs when you showing the component conditionally, for example:
showModal && <Modal onClose={toggleModal}/>
You can try to do some little tricks in the Modal onClose function, like
setTimeout(onClose, 0)
This works for me :')
const [state, setState] = useState({});
useEffect( async ()=>{
let data= await props.data; // data from API too
setState(users);
},[props.data]);
I had this problem in React Native iOS and fixed it by moving my setState call into a catch. See below:
Bad code (caused the error):
const signupHandler = async (email, password) => {
setLoading(true)
try {
const token = await createUser(email, password)
authContext.authenticate(token)
} catch (error) {
Alert.alert('Error', 'Could not create user.')
}
setLoading(false) // this line was OUTSIDE the catch call and triggered an error!
}
Good code (no error):
const signupHandler = async (email, password) => {
setLoading(true)
try {
const token = await createUser(email, password)
authContext.authenticate(token)
} catch (error) {
Alert.alert('Error', 'Could not create user.')
setLoading(false) // moving this line INTO the catch call resolved the error!
}
}
Similar problem with my app, I use a useEffect to fetch some data, and then update a state with that:
useEffect(() => {
const fetchUser = async() => {
const {
data: {
queryUser
},
} = await authFetch.get(`/auth/getUser?userId=${createdBy}`);
setBlogUser(queryUser);
};
fetchUser();
return () => {
setBlogUser(null);
};
}, [_id]);
This improves upon Carlos Vallejo's answer.
useEffect(() => {
let abortController = new AbortController();
// your async action is here
return () => {
abortController.abort();
}
}, []);
in the above code, I've used AbortController to unsubscribe the effect. When the a sync action is completed, then I abort the controller and unsubscribe the effect.
it work for me ....
The easy way
let fetchingFunction= async()=>{
// fetching
}
React.useEffect(() => {
fetchingFunction();
return () => {
fetchingFunction= null
}
}, [])
options={{
filterType: "checkbox"
,
textLabels: {
body: {
noMatch: isLoading ?
:
'Sorry, there is no matching data to display',
},
},
}}

React: Clean-Up function never called

I'm building a react app that uses Firebase and Firestore.
I'm using the onSnapshot function so I get the real time data from Firestore, but I was wondering how I can remove that listener.
Yeah I know, I must use the cleanup function of the useEffect hook, but I can't make it work, here's my code:
useEffect(() => {
let removeListener = getCanzoni(utente).onSnapshot(function (querySnapshot) {
let promises = querySnapshot.docs.map(async function (doc) {
let canzone = doc.data();
canzone.id = doc.id;
return canzone;
});
Promise.all(promises).then((canzoni) => {
cambiaCanzoni(canzoni);
});
return function cleanup() {
console.log("Removed Listener");
removeListener();
};
});
}, []);
The getCanzoni function is imported from another files and it's definition is:
export function getCanzoni(utente) {
return firestore
.collection("Canzoni")
.where("utente", "==", utente.uid)
.orderBy("data", "desc");
}
When I remove the component, I don't see the 'Removed Listener' in the console.
I know that the clean-up function is called when the dependency array changes or when the components is unmounted.
Any idea or tips?
I've found the error:
The clean up function must be defined in the hook's function body, not inside other function, like this:
useEffect(() => {
let removeListener = getCanzoni(utente).onSnapshot(function (querySnapshot) {
let promises = querySnapshot.docs.map(async function (doc) {
let canzone = doc.data();
canzone.id = doc.id;
return canzone;
});
Promise.all(promises).then((canzoni) => {
cambiaCanzoni(canzoni);
});
});
return function cleanup() {
console.log("Tolto il listener");
removeListener();
};
}, []);

Reactivity issue in IE browser

There is data that I receive from the server every 10 seconds in the Vuex repository:
From it I pull the data into the inside of the component through the calculated property:
computed: {
trackingStatusId: function () {
return this.$store.state.tracking.trackingServerData.tracking_status_id;
},
},
Next, I try to work with this data inside the method: And the method itself is attached to the styles in the html block(<i class="state" :style="getIndicatorWidth()"></i>)
methods: {
getIndicatorWidth: function () {
function widthCalculate(currentTrackingStatusId){
console.log(currentTrackingStatusId);
}
return widthCalculate(this.trackingStatusId);
},
},
Now we pay attention to this line in the above code:
console.log(currentTrackingStatusId);
If I get changed data from the server, then the value inside the console log changes reactively in all browsers except IE-11 (and below).
Getting data from the server is as follows:
(hung on a lifecycle hook mounted ())
methods: {
getTrackerIntervalData () {
setInterval(()=>myTimer(this), 5000);
function myTimer(th) {
return axios.get("https://seo-gmbh.eu/couriertracker/json/couriertracker_api.php?action=get_tracking_data&key_id=" + th.$route.params.tracking.toLowerCase() , {
})
.then(response => {
th.$store.commit('tracking/setTrackingServerData', response.data.data.tracking_data);
})
.catch(function (error) {
console.log(error);
});
}
},
}
Vuex repository:
const axios = require("axios");
export const state = () => ({
trackingServerData: null,
});
export const mutations = {
setTrackingServerData (state, trackingServerData) {
state.trackingServerData = trackingServerData;
},
};
I also duplicate the first request from middleware to be able to work with data when rendering a DOM tree:
export default function ({$axios, req, store, route}) {
if(req != undefined) {
store.commit('setIsFirstServerLoad', true);
}
else{
store.commit('setIsFirstServerLoad', false);
}
if(route.name == "language-tracker-tracking") {
return $axios.get("https://seo-gmbh.eu/couriertracker/json/couriertracker_api.php?action=get_tracking_data&key_id=" + route.params.tracking.toLowerCase(), {})
.then(response => {
store.commit('tracking/setTrackingServerData', response.data.data.tracking_data);
})
.catch(function (error) {
console.log(error);
});
}
}
As a result, if you look at the situation deeper, it turns out that the IE browser correctly calculates only the code received from middleware:
in the above screenshot, you can observe that the answer 8 - is correct - and the answer 3 is generally not clear where it came from.
(maybe even from the last page from the repository - although I had a separate repository for it there. I have no idea why this happens.)
Question:
Why data is not updated correctly in IE browsers?
How can this be fixed or is it possible to get around this problem - to implement everything differently?
the problem is here
getTrackerIntervalData () {
setInterval(()=>myTimer(this), 5000);
function myTimer(th) {
return axios.get("https://seo-gmbh.eu/couriertracker/json/couriertracker_api.php?action=get_tracking_data&key_id=" + th.$route.params.tracking.toLowerCase() , {
})
.then(response => {
th.$store.commit('tracking/setTrackingServerData', response.data.data.tracking_data);
})
.catch(function (error) {
console.log(error);
});
}
}
IE dosent support arrow function.
you should do it like so
getTrackerIntervalData () {
let vm=this;
setInterval(function(){myTimer(vm)}, 5000);
function myTimer(th) {
return axios.get("https://seo-gmbh.eu/couriertracker/json/couriertracker_api.php?action=get_tracking_data&key_id=" + th.$route.params.tracking.toLowerCase() , {
})
.then(response => {
th.$store.commit('tracking/setTrackingServerData', response.data.data.tracking_data);
})
.catch(function (error) {
console.log(error);
});
}
}
because vue use webpack. normally arrow function will be transform to normal function to support all browsers but maybe for some reason the setInterval with arrow function not transform. try it maybe that the problem

How to Cancel subscription in async promise to avoid memory leak in Reactjs

Within my React component, I have an async request which dispatches an action to my Redux store which is called within the useEffect hook:
const loadFields = async () => {
setIsLoading(true);
try {
await dispatch(fieldsActions.fetchFields(user.client.id));
} catch (error) {
setHasError(true);
}
setIsLoading(false);
}
useEffect(() => { if(isOnline) { loadFields() } }, [dispatch, isOnline]);
The action requests data via a fetch request:
export const fetchFields = clientId => {
return async dispatch => {
try {
const response = await fetch(
Api.baseUrl + clientId + '/fields',
{ headers: { 'Apiauthorization': Api.token } }
);
if (!response.ok) {
throw new Error('Something went wrong!');
}
const resData = await response.json();
dispatch({ type: SET_FIELDS, payload: resData.data });
} catch (error) {
throw error;
}
}
};
export const setFields = fields => ({
type : SET_FIELDS,
payload : fields
});
When this is rendered within the React app it results in the following 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 %s.%s, a useEffect cleanup function
I believe this occurs because the promise doesn't have a "clean-up" function. But I am unsure where to place this? Should I have some logic within LoadFields()? Or must this be done within the useEffect hook?
This tutorial which will help you to resolve your issue.
Quick example: with Promises
function BananaComponent() {
const [bananas, setBananas] = React.useState([])
React.useEffect(() => {
let isSubscribed = true
fetchBananas().then( bananas => {
if (isSubscribed) {
setBananas(bananas)
}
})
return () => isSubscribed = false
}, []);
return (
<ul>
{bananas.map(banana => <li>{banana}</li>)}
</ul>
)
}
Quick example: with async/await (Not the best one but that should work with an anonymous function)
function BananaComponent() {
const [bananas, setBananas] = React.useState([])
React.useEffect(() => {
let isSubscribed = true
async () => {
const bananas = await fetchBananas();
if (isSubscribed) {
setBananas(bananas)
}
})();
return () => isSubscribed = false
}, []);
return (
<ul>
{bananas.map(banana => <li>{banana}</li>)}
</ul>
)
}
First issue
If your useEffect() fetches data acynchronously then it would be a very good idea to have a cleanup function to cancel the non-completed fetch. Otherwise what could happen is like that: fetch takes longer than expected, meantime the component is re-rendered for whatever reason. Maybe because its parent is re-rendered. The cleanup of useEffect runs before re-render and the useEffect itself runs after re-render. To avoid having another fetch inflight it's better to cancel the previous one. Sample code:
const [data, setData] = useState();
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const apiData = await fetch("https://<yourdomain>/<api-path>",
{ signal: controller.signal });
setData(apiData);
} catch (err) {
if (err.name === 'AbortError') {
console.log("Request aborted");
return;
}
}
};
fetchData();
return () => {
controller.abort();
}
});
Second issue
This code
return async dispatch => {
will not work because neither dispatch nor Redux store support async actions. The most flexible and powerful way to handle this issue is to use middleware like redux-saga. The middleware lets you:
dispatch 'usual' sync actions to Redux store.
intercept those sync actions and in response make one or several async calls doing whatever you want.
wait until async call(s) finish and in response dispatch one or several sync actions to Redux store, either the original ones which you intercepted or different ones.

How to verify console.log was called in componentDidMount using Jest and Enzyme?

I'm trying test for cases when my axios call does not get an HTTP response of 200. When axios does not get a successful response, it throws an error. I want to verify that console.log gets called twice in this case.
Here's a snippet of the class I'm testing:
class App extends React.Component {
...
async componentDidMount() {
let url = "/api/info/tmp"
try {
let response = await axios.get(url);
...do stuff
this.setState(...);
} catch (e) {
console.log("Could not get " + url);
console.log(e);
}
}
...
}
And here's a snippet of my jest test
let mockAxios = new MockAdapter(axios);
...
describe("App - componentDidMount() behavior test", () => {
beforeEach(() => {
app = shallow(<App />);
})
afterEach(() => {
app = undefined;
mockAxios.reset();
});
...
describe("Get " + url + " HTTP response status is not 200", () => {
beforeAll(() => {
mockAxios.onGet(url).reply(302, mockData);
});
it("Does not set state regardless of response body", () => {
console.log = jest.fn();
const state = app.state();
expect(console.log).toHaveBeenCalledTimes(2);
expect(state.solutions).toEqual({});
expect(state.username).toEqual("");
});
});
});
I know the console.log = jest.fn() bit is doing something because the console does not log the fake error anymore when I set it. However, the test fails because Expected mock function to have been called two times, but it was called zero times.
I've tried moving the console.log = jest.fn() into the "beforeEach", "beforeAll", and as a global variable.
UPDATE
I am pretty sure it's something to do with all the async that is going on.
If I do this:
it("Does not set state regardless of response body", async () => {
console.log = jest.fn();
await app.instance().componentDidMount();
expect(console.log).toHaveBeenCalledTimes(2);
const state = app.state();
expect(state.solutions).toEqual({});
expect(state.username).toEqual("");
});
Then the test still fails but my reason changed: Expected mock function to have been called two times, but it was called four times. Now I just got to figure out why it was called four times not twice.
UPDATE 2
I figured out why console.log was being called 4 times! Now I just need to figure out how I should refactor my tests.
If I comment out my jest mock, and even the whole unit test
it("Does not set state regardless of response body", async () => {
//const state = app.state();
//expect(state.solutions).toEqual({});
//expect(state.username).toEqual("");
//expect(console.log).toHaveBeenCalledTimes(2);
});
Then I can count in my console that there are already indeed two different console.log calls. shallow(<App />) must be already calling componentDidMount() or something. When I add app.instance().componentDidMount(), I can visually see that it is logging 4 times.
Updated Answer
Since it looks like you already know what you're doing with mocks, perhaps the issue has to do with componentDidMount().
I believe that your call to shallow(<App />) will already call App's componentDidMount() one time (which means your console.log will get called twice there).
Then, you subsequently call app.instance().componentDidMount() - that is, you call componentDidMount() again (which means your console.log will get called twice there again).
So, total... 4 calls to console.log.
Hope that points you in the right direction...
Original Answer
Actually, your question looks quite similar to [this StackOverFlow question on how to "How to mock console when it is used by a third-party library?"
You can use Jest mock functions to spyOn the global.console object.
For example, your test may look like this:
// Setup jest to spy on the console
const consoleSpy = jest.spyOn(global.console, 'log')
describe('App - componentDidMount() behavior test', () => {
beforeEach(() => {
jest.resetAllMocks() // reset your consoleSpy state back to initial
app = shallow(<App />)
})
...
it('Does not set state regardless of response body', () => {
const spy = jest.spyOn(global.console, 'log')
const state = app.state()
expect(consoleSpy).toHaveBeenCalledTimes(2)
expect(state.solutions).toEqual({})
expect(state.username).toEqual('')
})
...
Ideally, you'd move your API call outside of componentDidMount and into its own class method. Thay way it can be manually invoked from a lifecycle method or from an event callback. Also, you should anticipate the response to affect your UI state in some fashion (example: displaying a message to the user that the request failed and to try again).
The following example can be done with .then/.catch instead of async/await. Either way, you're working with Promises that are asynchronous and therefore they need asynchronous tests.
Note: The below assumes disableLifecycleMethods is true in the enzyme adapter. Also, just testing state changes (or a console.log) is a bit superfluous; instead, you would test if a component is rendered based upon the current state.
Working example: https://codesandbox.io/s/939w229l9r (includes both end to end and integration tests --- you can run the tests by clicking on the Tests tab located near the bottom left of the sandbox)
App.js (this will be a container that holds all relevant state and disperses it to its children as needed)
import React, { Component } from 'react';
class App extends Component {
state = = {
error: "",
isLoading: true,
solutions: {},
username: ""
};
componentDidMount() {
this.fetchData("/api/info/tmp");
}
fetchData = async (url) => {
try {
const res = await axios.get(url);
...do stuff
this.setState({
error: "",
isLoading: false,
solutions: res.data.solutions,
username: res.data.username
});
} catch (err) {
this.setState({
error: err,
isLoading: false,
solutions: {},
username: ""
});
}
}
render() { ... }
}
App.test.js (this assumes you'd want an end to end test)
import { shallow } from 'enzyme';
import App from './App';
const timeout = () =>
new Promise(resolve => {
setTimeout(() => {
resolve();
}, 2000);
});
const initialState = {
error: "",
isLoading: true,
solutions: {},
username: ""
};
describe("App", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<App />);
wrapper.setState({ ...initialState });
});
afterAll(() => {
wrapper.unmount();
});
it("sets data to state based upon successful API call", async () => {
wrapper.instance().fetchData("/api/info/tmp");
await timeout();
wrapper.update();
expect(wrapper.state('isLoading')).toBeFalsy();
expect(wrapper.state('solutions')).toEqual({ somedata });
expect(wrapper.state('username')).toEqual("Some User");
});
it("displays an error upon unsuccessful API call", async () => {
wrapper.instance().fetchData("/api/bad/url");
await timeout();
wrapper.update();
expect(wrapper.state('isLoading')).toBeFalsy();
expect(wrapper.state('solutions')).toEqual({});
expect(wrapper.state('username')).toEqual("");
expect(wrapper.state('error')).toEqual("No data found.");
});
});
App.test.js (this assumes you'd want an integration test)
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import React from "react";
import { shallow } from "enzyme";
import App from "../App";
const solutions = [{ ... }, { ... }];
const username = "Some User"
const mockAxios = new MockAdapter(axios);
const initialState = {
error: "",
isLoading: true,
solutions: {},
username: ""
};
describe("App", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<App />);
wrapper.setState({ ...initialState });
});
afterEach(() => {
mock.reset();
});
afterAll(() => {
mock.restore();
wrapper.unmount();
});
it("displays an error upon unsuccessful API call", async () => {
try {
mockAxios.onGet("/users").networkErrorOnce();
await axios.get("users");
} catch (err) {
const error = err.toString();
wrapper.setState({
error,
isLoading: false,
solutions: {},
username: ""
});
wrapper.update();
expect(wrapper.state('isLoading')).toBeEqual(error);
expect(wrapper.state('isLoading')).toBeFalsy();
expect(wrapper.state('solutions')).toEqual({});
expect(wrapper.state('username')).toEqual("");
}
});
it("sets data to state based upon successful API call", async () => {
try {
mockAxios.onGet("/users").reply(200, { solutions, username });
const res = await axios.get("users");
wrapper.setState({
error: "",
isLoading: true,
solutions: res.data.solutions,
username: res.data.username
});
wrapper.update();
expect(wrapper.state('isLoading')).toBeFalsy();
expect(wrapper.state('solutions')).toEqual(solutions);
expect(wrapper.state('username')).toEqual(username);
} catch (e) {
console.log(e);
}
});
});
I figured it out! Kind of... I am not certain why it works like this, but setting the mock in the actual "it" did not work.
The solution was making a beforeEach and afterEach
describe("Get " + url + " HTTP response status is not 200", () => {
beforeAll(() => {
mockAxios.onGet(url).reply(302, mockData);
});
beforeEach(() => {
console.log = jest.fn();
});
afterEach(() => {
jest.resetAllMocks();
});
it("Does not set state regardless of response body", async () => {
const state = app.state();
expect(state.solutions).toEqual({});
expect(state.username).toEqual("");
expect(console.log).toHaveBeenCalledTimes(2);
});
});

Categories

Resources