Component router after timeout with state - javascript

I currently have a component that does a history.push('/') after a few seconds. But I am getting warnings
index.js:1375 Warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state.
and also
index.js:1375 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.
I am fairly new to React, do I need do some sort of clean up?
Here is my component.
import React, {useState} from 'react'
import {UsePostOrPutFetch} from "../hooks/postHook";
import "./styles/ConfirmationChange.scss";
import moment from 'moment';
export default function ConfirmatonChange(props) {
const [shouldFetch, setShouldFetch] = useState(false);
const [data,loading,isError, errorMessage] = UsePostOrPutFetch("/send-email/", props.data.location.state.value,"POST", shouldFetch, setShouldFetch);
const [countDown, setCountDown] = useState(5)
let spinner = (
<strong className="c-spinner" role="progressbar">
Loading…
</strong>
);
const changeView = () =>
{
if (countDown < 0) {
props.data.history.push('/')
} else {
setTimeout(() => {
setCountDown(countDown - 1)
}
, 1000)
}
}
return (
<div>
<div className="o-container">
<article className="c-tile">
<div className="c-tile__content">
<div className="c-tile__body u-padding-all">
<button className = "c-btn c-btn--primary u-margin-right" onClick={props.data.history.goBack}>Edit</button>
<button className = "c-btn c-btn--secondary u-margin-right" disabled={loading} onClick={(e) => { setShouldFetch(true)}}>Send</button>
{!loading && data === 200 && !isError ? (
<div className="ConfirmationChange-success-send">
<hr hidden={!data === 200} className="c-divider" />
Email Sent succesfully
<p>You will be redirected shortly....{countDown}</p>
{changeView()}
</div>
) : (loading && !isError ? spinner :
<div className="ConfirmationChange-error-send">
<hr hidden={!isError} className="c-divider" />
{errorMessage}
</div>
)}
</div>
</div>
</article>
</div>
</div>
)
}
And here is what my data fetch component look like
import { useState, useEffect } from "react";
import { adalApiFetch } from "../config/adal-config";
const UsePostOrPutFetch = (url, sendData, methodType, shouldFetch, setShouldSend) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [errorMessage, setError] = useState("");
useEffect(() => {
const ac = new AbortController();
if (shouldFetch) {
const postOrPutData = async () => {
try {
const response = await adalApiFetch(fetch, url,
{
signal: ac.signal,
method: methodType,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(sendData)
});
const json = await response.json();
setData(await json);
setLoading(true);
} catch (err) {
setIsError(true);
setError(err.message);
} finally {
setShouldSend(false)
setLoading(false);
}
};
postOrPutData();
}
return () => { ac.abort(); };
}, [shouldFetch, sendData, url, methodType, setShouldSend]);
return [data, loading, isError, errorMessage];
};
export {UsePostOrPutFetch}
Any help would be greatly appreciated.

Check React Hooks - Check If A Component Is Mounted
The most common cause of this warning is when a user kicks off an asynchronous request, but leaves the page before it completes.
You'll need a componentIsMounted variable and useEffect and useRef hooks :
const componentIsMounted = useRef(true);
useEffect(() => {
return () => {
componentIsMounted.current = false;
};
}, []);
const changeView = () => {
if (countDown < 0) {
props.data.history.push("/");
} else {
setTimeout(() => {
if (componentIsMounted.current) { // only update the state if the component is mounted
setCountDown(countDown - 1);
}
}, 1000);
}
};
You should do the same for data fetch component

Yup, you have a timeOut that could potentially fire after the component has unmounted.
You need to add a useEffect that clears the timer onUnmount as follows
const timerRef = useRef();
useEffect(() => () => clearTimeout(timerRef.current), [])
const changeView = () => {
if (countDown < 0) {
props.data.history.push("/");
} else {
timerRef.current = setTimeout(() => {
setCountDown(countDown - 1);
}, 1000);
}
};

Related

React-router-dom, useNavigate(-1) problem

I am working on countries project.
I get information about this when the border buttons are clicked. But when I click the Back button, the previous country data does not appear. How can I fix this? Please help me!
Here is my Country Component
import React, { useEffect, useState } from 'react';
import { Link, useParams, useNavigate } from 'react-router-dom';
import Loading from './Loading';
function Country() {
const { countryCode } = useParams();
const navigate = useNavigate();
const [country, setCountry] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => { getSingleCountryData(countryCode); }, []);
const getSingleCountryData = async (countryCode) => {
setLoading(true);
try {
const country = JSON.parse(localStorage.getItem("countries")).filter(c => c.cca3 === countryCode);
setCountry(country[0]);
setLoading(false);
} catch (err) {
console.log(err);
}
}
return loading ? <Loading />
: (
<div className='country container'>
<button className='btn backBtn' onClick={() => navigate(-1)}> Back </button>
// some code
<div className="country__borders">
<h4>Border Countries:</h4>
{country.borders && country.borders.map((border, index) => {
return <Link to={`/countries/${border}`} onClick={() => getSingleCountryData(border)} key={index} className='btn'>{border}</Link>
})}
</div>
</div>
);
}
export default Country;
The useEffect hook is missing the getSingleCountryData and countryCode as dependencies. You'll want to memoize the getSingleCountryData callback so it's provided to the useEffect hook as a stable callback reference.
const getSingleCountryData = useCallback(async (countryCode) => {
setLoading(true);
try {
const country = (JSON.parse(localStorage.getItem("countries")) || [])
.filter(c => c.cca3 === countryCode);
setCountry(country[0]);
} catch (err) {
console.log(err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
getSingleCountryData(countryCode);
}, [countryCode, getSingleCountryData]);
Since the country data is fetched and loaded when the countryCode route param updates there'll no longer be a need to trigger this fetching when the link is clicked.
{country.borders && country.borders.map((border, index) => (
<Link
key={index}
to={`/countries/${border}`}
className='btn'
>
{border}
</Link>
))}
And since the data is not longer fetched via a click handler and only referenced in the useEffect, getSingleCountryData can be moved into the useEffect hook and be removed entirely as a dependency.
const getSingleCountryData = useCallback(, []);
useEffect(() => {
const getSingleCountryData = async (countryCode) => {
setLoading(true);
try {
const country = (JSON.parse(localStorage.getItem("countries")) || [])
.filter(c => c.cca3 === countryCode);
setCountry(country[0]);
} catch (err) {
console.log(err);
} finally {
setLoading(false);
}
}
getSingleCountryData(countryCode);
}, [countryCode]);

How to call api only on mount and click of Fetch Next User button

I have the below code
import React, {useState, useEffect, useCallback} from 'react';
import axios from 'axios';
const Users = () => {
const [users, setUsers] = useState([]);
const [nextPageNumber, setNextPageNumber] = useState(1);
const fetchUsers = useCallback(() => {
axios.get(`https://randomuser.me/api?page=${nextPageNumber}`).then(response => {
const updatedUsers = [...users, ...response.data.results];
setUsers(updatedUsers);
setNextPageNumber(response.data.info.page + 1);
}).catch(error => {
console.log(error)
})
}, [nextPageNumber, users])
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
const fetchNextUser = () => {
fetchUsers();
}
if(users.length === 0){
return <div>No Users available</div>
}
return(
<div>
<button onClick={fetchNextUser}>Fetch Next user</button>
{users.map(user => (
<div key={user.id}>
<div>Name: {user.name.title} {user.name.first} {user.name.last}</div>
<div>
<img src={user.picture.large} alt="Not available"/>
</div>
</div>
))}
</div>
)
}
export default Users;
This is calling the api repeatadlly. I have used the fetchUsers dependency in useEffect and useCallback hook due to eslint errors. I just want to call the api on first mount and on click of Fetch Next user button without any eslint error.
Is there any way we can achieve that?
Have a try with the below changes it will not give you the eslint error messages.
import React, {useState, useEffect, useRef, useCallback} from 'react';
import axios from 'axios';
const Users = () => {
const [users, setUsers] = useState([]);
const nextPageRef = useRef(1)
const fetchUsers = useCallback(() => {
axios.get(`https://randomuser.me/api?page=${nextPageRef.current}`).then(response => {
const updatedUsers = [...response.data.results];
setUsers(prevUsers => [...prevUsers, ...updatedUsers]);
nextPageRef.current = response.data.info.page + 1
}).catch(error => {
console.log(error)
})
}, [])
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
You could simply use
useEffect(() => {
fetchUsers();
}, []);
This will call the fetchUsers function only once.
And when button is pressed, again call the fetchUsers() function

wait 500 miliseconds before running react hook

I have built this custom react hook :
import { useEffect, useContext, useState } from 'react';
import { ProductContext } from '../contexts';
import axios from 'axios';
export default function useProducts(searchQuery) {
const [products, setProducts] = useContext(ProductContext);
const [isLoading, setIsloading] = useState(true);
useEffect(() => {
axios
.get(`/shoes?q=${searchQuery ? searchQuery : ''}`)
.then((res) => {
setProducts(res.data);
setIsloading(false);
})
.catch((err) => {
console.error(err);
});
}, [searchQuery, setProducts]);
return { products, isLoading };
}
It basically fetches some data based on a query string that i pass in. The query string comes from an input field :
import React, { useState } from 'react';
import { FiSearch } from 'react-icons/fi';
import { useProducts } from '../../hooks';
export default function SearchBar() {
const [query, setQuery] = useState('');
const handleChange = (e) => {
e.preventDefault();
setQuery(e.target.value);
};
useProducts(query);
return (
<div className="search-form">
<FiSearch className="search-form__icon" />
<input
type="text"
className="search-form__input"
placeholder="Search for brands or shoes..."
onChange={handleChange}
/>
</div>
);
}
The problem is it will fetch while the user is typing. I want it to fetch after the user didnt type for 500 miliseconds.
What I tried is :
setTimeout(() => {
useProducts(query);
}, 500);
But this will return an error saying :
src\components\header\SearchBar.js
Line 14:5: React Hook "useProducts" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks
Search for the keywords to learn more about each error.
You can debounce your value with an additional piece of state. Once query is changed, we set off a 500 ms timer that will set the value of debounced. However, if the effect re-runs, we clear that timer and set a new timer.
import React, { useState, useEffect } from 'react';
import { FiSearch } from 'react-icons/fi';
import { useProducts } from '../../hooks';
export default function SearchBar() {
const [query, setQuery] = useState('');
const [debounced, setDebounced] = useState('');
useEffect(() => {
const timeout = setTimeout(() => {
setDebounced(query);
}, 500);
return () => { clearTimeout(timeout) }
}, [query])
const handleChange = (e) => {
e.preventDefault();
setQuery(e.target.value);
};
useProducts(debounced);
return (
<div className="search-form">
<FiSearch className="search-form__icon" />
<input
type="text"
className="search-form__input"
placeholder="Search for brands or shoes..."
onChange={handleChange}
/>
</div>
);
}
I'd change the useProducts hook to accept a debounce time as a parameter, and have it make the axios call only once the debounce time is up:
useProducts(query, 500);
export default function useProducts(searchQuery, debounceTime = 0) {
const [products, setProducts] = useContext(ProductContext);
const [isLoading, setIsloading] = useState(true);
const [timeoutId, setTimeoutId] = useState();
useEffect(() => {
clearTimeout(timeoutId);
setTimeoutId(setTimeout(() => {
axios
.get(`/shoes?q=${searchQuery ? searchQuery : ''}`)
.then((res) => {
setProducts(res.data);
setIsloading(false);
})
.catch((err) => {
console.error(err);
});
}, debounceTime));
}, [searchQuery, setProducts]);
return { products, isLoading };
}

Get from api in useEffect and render components accordingly

Im having troubles rendering components based on api calls in React. I fetch my data in useEffect hook update a state with the data. The state is null for a while before the api get all the data but by that time, the components are rendering with null values. This is what I have:
import React, { useEffect, useState } from 'react'
import axios from 'axios';
import { Redirect } from 'react-router-dom';
const Poll = (props) => {
const [poll, setPoll] = useState(null);
//if found is 0 not loaded, 1 is found, 2 is not found err
const [found, setFound] = useState(0);
useEffect(() => {
axios.get(`api/poll/${props.match.params.id}`)
.then(res => {
console.log(res.data);
setPoll(res.data);
setFound(1);
})
.catch(err => {
console.log(err.message);
setFound(2);
});
}, [])
if(found===2) {
return(
<Redirect to="/" push />
)
}else{
console.log(poll)
return (
<div>
</div>
)
}
}
export default Poll
That is my workaround but it doesnt feel like thats the way it should be done. How can I set it so that I wait for my api data to get back then render components accordingly?
You don't need to track the state of the API call like const [found, setFound] = useState(1). Just check if poll exists and also you can create a new state variable for tracking the error.
For example if (!poll) { return <div>Loading...</div>} this will render a div with 'loading...' when there is no data. See the code below, for complete solution,
import React, { useEffect, useState } from 'react'
import axios from 'axios';
import { Redirect } from 'react-router-dom';
const Poll = (props) => {
const [poll, setPoll] = useState(null);
const [hasError, setHasError] = useState(false);
useEffect(() => {
axios.get(`api/poll/${props.match.params.id}`)
.then(res => {
console.log(res.data);
setPoll(res.data);
})
.catch(err => {
console.log(err.message);
setHasError(true)
});
}, [])
if(!poll) {
console.log('data is still loading')
return(
<div>Loading....</div>
)
}
if (hasError) {
console.log('error when fetching data');
return (
<Redirect to="/" push />
)
}
return (
<div>
{
poll && <div>/* The JSX you want to display for the poll*/</div>
}
</div>
);
}
export default Poll
In your than, try to use a filter:
setPoll(poll.filter(poll => poll.id !== id));
Make sure to replace id by your identificator
The standard way is to have other variables for the loading and error states like this
const Poll = (props) => {
const [poll, setPoll] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
setLoading(true);
axios.get(`api/poll/${props.match.params.id}`)
.then(res => {
console.log(res.data);
setPoll(res.data);
})
.catch(err => {
console.log(err.message);
setError(true);
})
.finally(()=> {
setLoading(false);
};
}, [])
if(error) return <span>error<span/>
if(loading) return <span>loading<span/>
return (
<div>
// your poll data
</div>
)
}

How to wait until context value is set?

I'm trying to render a header.
First, in InnerList.js, I make an API call, and with the data from the API call, I set a list in context.
Second, in Context.js, I take the list and set it to a specific data.
Then, in InnerListHeader.js, I use the specific data to render within the header.
Problem: I currently get a TypeError undefined because the context is not set before rendering. Is there a way to wait via async or something else for the data to set before loading?
My code block is below. I've been looking through a lot of questions on StackOverflow and blogs but to no avail. Thank you!
InnerList.js
componentDidMount() {
const { dtc_id } = this.props.match.params;
const {
setSpecificDtcCommentList,
} = this.context;
MechApiService.getSpecificDtcCommentList(dtc_id)
.then(res =>
setSpecificDtcCommentList(res)
)
}
renderSpecificDtcCommentListHeader() {
const { specificDtc = [] } = this.context;
return (
<InnerDtcCommentListItemHeader key={specificDtc.id} specificDtc={specificDtc} />
)
}
Context.js
setSpecificDtcCommentList = (specificDtcCommentList) => {
this.setState({ specificDtcCommentList })
this.setSpecificDtc(specificDtcCommentList)
}
setSpecificDtc = (specificDtcCommentList) => {
this.setState({ specificDtc: specificDtcCommentList[0] })
}
InnerListHeader.js
render() {
const { specificDtc } = this.props;
return (
<div>
<div className="InnerDtcCommentListItemHeader__comment">
{specificDtc.dtc_id.dtc}
</div>
</div>
);
}
In general, you should always consider that a variable can reach the rendering stage without a proper value (e.g. unset). It is up to you prevent a crash on that.
For instance, you could rewrite you snippet as follows:
render() {
const { specificDtc } = this.props;
return (
<div>
<div className="InnerDtcCommentListItemHeader__comment">
{Boolean(specificDtc.dtc_id) && specificDtc.dtc_id.dtc}
</div>
</div>
);
}
When you make an api call you can set a loader while the data is being fetched from the api and once it is there you show the component that will render that data.
In your example you can add a new state that will pass the api call status to the children like that
render() {
const { specificDtc, fetchingData } = this.props;
if (fetchingData){
return <p>Loading</p>
}else{
return (
<div>
<div className="InnerDtcCommentListItemHeader__comment">
{specificDtc.dtc_id.dtc}
</div>
</div>
);
}
}
``
in my case, i am calling external api to firebase which lead to that context pass undefined for some values like user. so i have used loading set to wait untile the api request is finished and then return the provider
import { createContext, useContext, useEffect, useState } from 'react';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
GoogleAuthProvider,
signInWithPopup,
updateProfile
} from 'firebase/auth';
import { auth } from '../firebase';
import { useNavigate } from 'react-router';
import { create_user_db, get_user_db } from 'api/UserAPI';
import { CircularProgress, LinearProgress } from '#mui/material';
import Loader from 'ui-component/Loader';
const UserContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState();
const [user_db, setUserDB] = useState();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
const createUser = async (email, password) => {
const user = await createUserWithEmailAndPassword(auth, email, password);
};
const signIn = (email, password) => {
return signInWithEmailAndPassword(auth, email, password)
.then(() => setIsAuthenticated(true))
.catch(() => setIsAuthenticated(false));
};
const googleSignIn = async () => {
const provider = new GoogleAuthProvider();
await signInWithPopup(auth, provider)
.then(() => setIsAuthenticated(true))
.catch(() => setIsAuthenticated(false));
};
const logout = () => {
setUser();
return signOut(auth).then(() => {
window.location = '/login';
});
};
const updateUserProfile = async (obj) => {
await updateProfile(auth.currentUser, obj);
return updateUser(obj);
};
const updateUser = async (user) => {
return setUser((prevState) => {
return {
...prevState,
...user
};
});
};
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
setLoading(true);
if (currentUser) {
const user_db = await get_user_db({ access_token: currentUser.accessToken });
setUserDB(user_db);
setUser(currentUser);
setIsAuthenticated(true);
}
setLoading(false);
});
return () => {
unsubscribe();
};
}, []);
if (loading) return <Loader />;
return (
<UserContext.Provider value={{ createUser, user, user_db, isAuthenticated, logout, signIn, googleSignIn, updateUserProfile }}>
{children}
</UserContext.Provider>
);
};
export const UserAuth = () => {
return useContext(UserContext);
};

Categories