useEffect dependencies not re-rendering component - reactjs

I'm trying to re-render a component using useEffect and passing dependencies, but it isn't working. I'm quite new to react hooks so I think I might not be passing the correct dependency. I'm fetching some info and updating the state, however, when I passed the dependencies the re-render cycle does not happen.
Here is the code:
import React, { useRef, useState, useEffect } from "react";
import Loader from "../UI/loader/loader";
import axios from "axios";
import "./surveybox.css";
interface surveryAnswer {
id: number;
answers: string[];
}
const SurveyBox: React.FC = () => {
const [surveyUserAnswers, setSurveyUserAnswers] = useState<surveryAnswer>();
const [loading, setLoading] = useState(false);
const [numberOfAnswers, setNumberOfAnswers] = useState<number>();
const programmingQRef = useRef<HTMLSelectElement>(null);
const skillsQRef = useRef<HTMLSelectElement>(null);
const stateManagementQRef = useRef<HTMLSelectElement>(null);
const programmerTypeQRef = useRef<HTMLSelectElement>(null);
useEffect(() => {
axios
.get(`${DB_URL}/users-answers.json`)
.then((res) => {
const fetchedAnswers = [];
for (let item in res.data) {
fetchedAnswers.push({
...res.data[item],
id: item,
});
}
setNumberOfAnswers(fetchedAnswers.length);
})
.catch((err) => {});
}, [numberOfAnswers, setNumberOfAnswers]);
const onSubmitSurvey = (e: React.FormEvent): void => {
e.preventDefault();
setLoading((prevLoading) => !prevLoading);
const newAnswer = {
id: Math.random(),
answers: [
programmerTypeQRef.current!.value,
skillsQRef.current!.value,
stateManagementQRef.current!.value,
programmerTypeQRef.current!.value,
],
};
setSurveyUserAnswers(newAnswer);
axios
.post(`${DB_URL}/users-answers.json`, newAnswer)
.then((res) => {
setLoading((prevLoading) => !prevLoading);
})
.catch((error) => {
console.log(error);
setLoading((prevLoading) => !prevLoading);
});
};
return (
<div className="surveybox-container">
{loading ? (
<div className={"loader-holder"}>
<Loader />
</div>
) : (
<React.Fragment>
<h2>Quick survey!</h2>
<form action="submit" onSubmit={onSubmitSurvey}>
<label>favorite programming framework?</label>
<select ref={programmingQRef} name="programming">
<option value="React">React</option>
<option value="Vue">Vue</option>
<option value="Angular">Angular</option>
<option value="None of the above">None of the above</option>
</select>
<br></br>
<label>what a junior developer should have?</label>
<select ref={skillsQRef} name="skills">
<option value="Eagerness to lear">Eagerness to learn</option>
<option value="CS Degree">CS Degree</option>
<option value="Commercial experience">
Commercial experience
</option>
<option value="Portfolio">Portfolio</option>
</select>
<br></br>
<label>Redux or Context Api?</label>
<select ref={stateManagementQRef} name="state-management">
<option value="Redux">Redux</option>
<option value="Context Api">Context Api</option>
</select>
<br></br>
<label>Backend, Frontend, Mobile?</label>
<select ref={programmerTypeQRef} name="profession">
<option value="Back-end">back-end</option>
<option value="Front-end">front-end</option>
<option value="mobile">mobile</option>
</select>
<br></br>
<button type="submit">submit</button>
</form>
<p>answered by {numberOfAnswers} visitors</p>
</React.Fragment>
)}
</div>
);
};
I'm passing the two dependencies that are involved in the useEffect and changes but still not working.
Thank you!
Edited Solution after recommendation:
...
interface surveryAnswer {
id: number;
answers: string[];
}
const SurveyBox: React.FC = () => {
const [surveyUserAnswers, setSurveyUserAnswers] = useState<surveryAnswer>();
const [loading, setLoading] = useState(false);
const [numberOfAnswers, setNumberOfAnswers] = useState<number>(0);
const programmingQRef = useRef<HTMLSelectElement>(null);
const skillsQRef = useRef<HTMLSelectElement>(null);
const stateManagementQRef = useRef<HTMLSelectElement>(null);
const programmerTypeQRef = useRef<HTMLSelectElement>(null);
const fetchAnswersFromServer = () => {
axios
.get(`${DB_URL}/users-answers.json`)
.then((res) => {
const fetchedAnswers = [];
for (let item in res.data) {
fetchedAnswers.push({
...res.data[item],
id: item,
});
}
setNumberOfAnswers(fetchedAnswers.length);
})
.catch((err) => {});
};
const onSubmitSurvey = (e: React.FormEvent): void => {
e.preventDefault();
setLoading((prevLoading) => !prevLoading);
const newAnswer = {
id: Math.random(),
answers: [
programmerTypeQRef.current!.value,
skillsQRef.current!.value,
stateManagementQRef.current!.value,
programmerTypeQRef.current!.value,
],
};
setSurveyUserAnswers(newAnswer);
axios
.post(`${DB_URL}/users-answers.json`, newAnswer)
.then((res) => {
fetchAnswersFromServer();
setLoading((prevLoading) => !prevLoading);
})
.catch((error) => {
console.log(error);
setLoading((prevLoading) => !prevLoading);
});
};
return (...

You probably only want the HTTP call to be made once (when the component mounts), so use [] as the dependency list. The HTTP call doesn't depend on any other props or state, so this most likely what you want.
If you want it to run more often than that, then you'll need to decide what data changes that requires you to make the HTTP call again, and add that to the dependency list instead of numberOfAnswers or setNumberOfAnswers.
The problem is that your effect lists numberOfAnswers as a dependency, and it also updates numberOfAnswers when it runs. So, this is likely what's happening:
The component mounts. numberOfAnswers is undefined by default.
The effect runs for the first time.
The HTTP request returns, say with 10 answers. It calls setNumberOfAnswers(10)
The component rerenders with numberOfAnswers=10
The effect runs again, because numberOfAnswers changed.
The HTTP request returns, probably with the same 10 answers. It calls setNumberOfAnswers(10)
The component does not rerender again because the value hasn't changed.

Issue
numberOfAnswers doesn't appear to be a dependency as it's not referenced in the effect callback, and secondly, unconditionally calling setNumberOfAnswers to update the numberOfAnswers can likely create an infinite loop. I suspect this effect runs once on mount, updates the state and runs a second time, and updates state again to the same value and then never updates again since the state is the same value.
Solution
So I was thinking to re-render every time there is a new submission.
You can trigger the useEffect on the surveyUserAnswers being updated.
Additional suggestions:
No need to create an new array and push elements into it just to compute the number of answers. Use the res.data.length if an array, or Object.values(res.data).length if it's an object.
Don't toggle the loading state, just set to true when starting the asynchronous loading action, and use a finally block to clear the loading state regardless if the Promise resolved or rejected.
Code:
const SurveyBox: React.FC = () => {
const [surveyUserAnswers, setSurveyUserAnswers] = useState<surveryAnswer>();
const [loading, setLoading] = useState(false);
const [numberOfAnswers, setNumberOfAnswers] = useState<number>();
const programmingQRef = useRef<HTMLSelectElement>(null);
const skillsQRef = useRef<HTMLSelectElement>(null);
const stateManagementQRef = useRef<HTMLSelectElement>(null);
const programmerTypeQRef = useRef<HTMLSelectElement>(null);
useEffect(() => {
axios
.get(`${DB_URL}/users-answers.json`)
.then((res) => {
setNumberOfAnswers(res.data.length); // or Object.values(res.data).length
})
.catch((err) => {});
}, [surveyUserAnswers]);
const onSubmitSurvey = (e: React.FormEvent): void => {
e.preventDefault();
setLoading(true);
const newAnswer = {
id: Math.random(),
answers: [
programmerTypeQRef.current!.value,
skillsQRef.current!.value,
stateManagementQRef.current!.value,
programmerTypeQRef.current!.value,
],
};
setSurveyUserAnswers(newAnswer);
axios
.post(`${DB_URL}/users-answers.json`, newAnswer)
.then((res) => {
// handle any processing
})
.catch((error) => {
console.log(error);
// handle any other error processing
})
.finally(() => setLoading(false));
};
Alternative Solution
Define onSubmitSurvey async and handle all the asynchronous logic via awaits and completely remove the useEffect hook.
const onSubmitSurvey = async (e: React.FormEvent): void => {
e.preventDefault();
setLoading(true);
const newAnswer = {
id: Math.random(),
answers: [
programmerTypeQRef.current!.value,
skillsQRef.current!.value,
stateManagementQRef.current!.value,
programmerTypeQRef.current!.value,
],
};
setSurveyUserAnswers(newAnswer);
try {
await axios.post(`${DB_URL}/users-answers.json`, newAnswer);
const res = await axios.get(`${DB_URL}/users-answers.json`);
setNumberOfAnswers(res.data.length); // or Object.values(res.data).length
} catch(error) {
console.log(error);
// handle any other error processing
} finally {
setLoading(false);
}
};

Related

How to Search by name using the Rest country Api

When i search for a country, The searchInput useState works fine and receives the value, problem is it doesn't update the country in the Dom immediately unless i remove the useEffect hook dependency array, And this causes too many re-renders, So how can i update the DOM when i search search, Here is my code.
const countryUrl = `https://restcountries.com/v2/name`;
const [searchInput, setSearchInput] = useState<string>("Nigeria");
const [countryData, setCountryData] = useState<object>([]);
const fetchCountry = (searchInput: any) => {
axios
.get(`${countryUrl}/${searchInput}?fullText=true`)
.then((res) => setCountryData(res.data[0]))
.catch((err) => console.log(err));
};
useEffect(() => {
fetchCountry(searchInput);
}, []);
const handleSubmit = (e: any) => {
e.preventDefault();
fetchWeather(searchInput);
fetchCountry();
};
<form onSubmit={handleSubmit}>
<input
onChange={(e) => setSearchInput(e.target.value)}
placeholder="Enter The Country"
type="text"
/>
<button type="submit">{<CiSearch />} </button>
</form>
Remove searchInput from fetchCountry = (searchInput: any) => {, it will look like fetchCountry = () => {
Reason is you have searchInput in useState so no need to pass as foo param
your fetchCountry will look like
const fetchCountry = () => { // removed params
axios
.get(`${countryUrl}/${searchInput}?fullText=true`)
.then((res) => setCountryData(res.data[0]))
.catch((err) => console.log(err));
};

Axios + React (Next.js), post request loses track of the context / state

Been scratching my head for hours, this bug doesn't make any sense to me.
Similarly to this solved problem, the react context and state is lost after the request is complete. The only problem is that all my functions are already bound.
The code below should return an image url and add it to the "image" array state every time the button is clicked.
What's not working is that the contents of the "image" array state is magically forgotten inside the axios promise, meaning that all previous images are lost, and only the image returned by the request will end up in the state.
I never had something like this happen before, feel like i must be missing some incredibly stupid mistake i've made. Anyone have any ideas what might be going on here?
Thank you,
// Added
const RANDOM_PROMPTS = [
'a whale flying in the sky',
'a skeleton dancing',
'a sunflower growing in a field'
]
export default function Home() {
const [prompt, setPrompt] = useState('')
const [images, setImages] = useState([])
// Added
useEffect(() => {
randomPrompt()
}, [])
const handleSubmit = event => {
event.preventDefault()
event.stopPropagation()
axios.post('http://localhost:3000/v1/generate', {prompt})
.then(res => {
res.data && add(res.data.url)
})
}
const add = (url) => {
// images state is always [] here
console.log(images)
setImages([...images, url])
}
// Added
const randomPrompt = () => {
const prompt = RANDOM_PROMPTS.shift()
setPrompt(`a painting of ${prompt}, by Paul Delvaux`)
}
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" value={prompt} onChange={event => setPrompt(event.target.value)}/>
<button type="submit">Draw</button>
</form>
</div>
)
}
Did you try to make youre function handleSubmit async function ?
const handleSubmit = async (event) => {
event.preventDefault()
event.stopPropagation()
const {data} = await axios.post('http://localhost:3000/v1/generate',
{prompt},{
validateStatus: (status) => status === 201,
})
if(!!data && !!data.url) {
add(data.url)
}
}
Je rajouterais que tu peux modifier ta fonction add comme suit:
const add = (url) => {
console.log(images)
setImages((prevState) => [...prevState, url])
}
My opinion is during the .then you may lose the contexte of youre react state.
Random Prompts should not have any effect on the Axios request you are making on a different useEffect() dependency. Please try my code below which I tried to do what I suggested in my comment.
const RANDOM_PROMPTS = [
'a whale flying in the sky',
'a skeleton dancing',
'a sunflower growing in a field'
]
export default function Home() {
const [prompt, setPrompt] = useState('')
const [images, setImages] = useState([])
const [initiateRequest, setInitiateRequest] = useState(false)
// Added
useEffect(() => {
randomPrompt()
}, [])
async function fetchData() {
let response = axios.post('http://localhost:3000/v1/generate', {prompt})
.then(res => {
res.data && add(res.data.url)
})
}
useEffect(() => {
fetchData()
}, [initiateRequest])
const handleSubmit = event => {
event.preventDefault()
event.stopPropagation()
setInitiateRequest(true)
}
const add = (url) => {
// images state is always [] here
console.log(images)
setImages([...images, url])
}
// Added
const randomPrompt = () => {
const prompt = RANDOM_PROMPTS.shift()
setPrompt(`a painting of ${prompt}, by Paul Delvaux`)
}
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" value={prompt} onChange={event => setPrompt(event.target.value)}/>
<button type="submit">Draw</button>
</form>
</div>
)
}

OnChange Event Stuck in Loop, Crashing

I am getting an infinite loop / crash here.
I'm trying to get an onChange event fired for these radio buttons (which are built after pulling data from a query), but I think it keeps redrawing and I can't figure out why.
Any thoughts on how I can solve this?
const GetChallenge = async () => {
const slug = useParams()
const data = await shopifyApolloClient.query({ query: singleProduct(slug) })
return data
}
const Challenge = () => {
let [loaded, setLoaded] = useState(false)
let [product, setProduct] = useState([])
let [variants, setVariants] = useState([])
let [options, setOption] = useState()
let [metafields, setMetafields] = useState([])
GetChallenge().then((ret) => {
setProduct(ret.data.product)
setVariants(ret.data.product.variants.edges)
setOption(ret.data.product.variants.edges[0].node.title)
setMetafields(ret.data.product.metafields.edges)
setLoaded(true)
})
const handleOptions = (event) => {
setOption(event.target.value)
}
if (loaded === true) {
return (
<div>
{variants.map((e) => (
<label
htmlFor={e.node.title}
key={e.node.id}>
<input
type="radio"
name="options"
checked={e.node.title === options}
value={e.node.title}
onChange={handleOptions}
/>
{e.node.title}
</label>
))}
</div>
)
} else {
return (
<p>Not Loaded</p>
)
}
}
GetChallenge is triggering every render. Try useEffect with the empty array empty soas to trigger only onmount.
import React, { useState, useEffect } from 'react';
const GetChallenge = async () => {
...
useEffect(() => {
GetChallenge().then((ret) => {
setProduct(ret.data.product)
setVariants(ret.data.product.variants.edges)
setOption(ret.data.product.variants.edges[0].node.title)
setMetafields(ret.data.product.metafields.edges)
setLoaded(true)
}),[]}
...
}
Try this:
onChange={(event) => setOption(event.target.value)}
hope this will solve your problem :)
import React, { useState, useEffect } from 'react';
const GetChallenge = async () => {
const slug = useParams();
const data = await shopifyApolloClient.query({ query: singleProduct(slug) });
return data;
};
const Challenge = () => {
const [data, setData] = useState({
loaded: false,
product: [],
variants: [],
options: "",
metafields: []
});
const { loaded, variants, options } = data;
useEffect(() => {
GetChallenge().then((ret) => {
setData((prevState) => ({
...prevState,
product: ret.data.product,
variants: ret.data.product.variants.edges,
options: ret.data.product.variants.edges[0].node.title,
metafields: ret.data.product.metafields.edges,
loaded: true
}));
});
}, []);
const handleOptions = (event) => {
setData((prevState) => ({ ...prevState, options: event.target.value }));
};
if (loaded === true) {
return (
<div>
{variants.map((e) => (
<label htmlFor={e.node.title} key={e.node.id}>
<input
type="radio"
name="options"
checked={e.node.title === options}
value={e.node.title}
onChange={(event) => handleOptions(event)}
/>
{e.node.title}
</label>
))}
</div>
);
} else {
return <p>Not Loaded</p>;
}
};

UseEffect is not called

I have a question about useEffect. My useEffect is not fetching the data the first time, I have to switch route for it to have the data I needed
const Comments = ({ ...rest }) => {
const theme = useTheme();
const classes = useStyles({ theme });
const [users, setUsers] = useState([]);
const { push } = useHistory();
const { token, loading } = useContext(AuthContext)
const dispatch = useDispatch();
const allUsers = useSelector(state => state.allUsers);
const comments = useSelector(state => state.listCommentsByBookId);
const listBooks = useSelector((state) => state.userListBooks);
const isFetching = useSelector((state) => state.isFetching);
const [stateReady, setReadyForRender] = useState(false)
const redirectTo = ( rowData ) => {
push({
pathname: ROUTE.USERS_DETAILS,
user: rowData
});
}
const options = {
filterType: 'checkbox',
selectableRowsHeader: false,
selectableRowsHideCheckboxes: false,
selectableRowsOnClick: false,
onRowClick: redirectTo,
};
const getAllComments = async () => {
var allusersId = [];
//get all ids
await allUsers.map((user) => {
allusersId.push(user.uid);
})
//get all books from users
await allusersId.map(async (id) => {
await dispatch(getUserListBooks(apiURL + `api/bdd/userListBooks/${id}`, token))
})
var listArray = [];
//filter the array and delete empty rows
listArray.push(listBooks);
var newArray = listArray.filter(e => e);
//map every user and stock the list of books in string
await newArray.forEach(async (book)=> {
await book.map(async (book) => {
await dispatch(getCommentsByBookId(apiURL + `api/bdd/ratingByBook/${book.id}`, token));
})
})
setReadyForRender(true)
}
useEffect(() => {
console.log('is fetching', isFetching)
if(comments.length === 0) {
getAllComments();
}
}, [stateReady])
console.log('COM', comments);
return (
<div>
{stateReady &&
<Card>
<Box className={classes.tableContainer} sx={{ minWidth: 1050 }}>
<MUIDataTable
data={comments}
columns={columns}
options={options}
/>
</Box>
</Card>}
</div>
);
};
Why? It might be related to async await but I'm stuck here.
If you want to fetch these informations on the first render, you'll have to pass an empty array as the second parameter of your useEffect.
The reason your useEffect is not called is because stateReady does not change during the course of your current code.
See this link, particularly the note section, it explains way better than me how the empty array as second parameter works.
Can you replace the useEffect section to the below code:
useEffect(() => {
(async () => {
console.log('is fetching', isFetching)
if(comments.length === 0) {
getAllComments();
}
})()
}, [stateReady])
You can read more about this in this link
You can use eslint to show errors when coding with hooks. In this case if you want useEffect to handle stateReady, please provide it in the function getAllComments() => getAllComments(stateReady) and when you call this function in useEffect with [stateReady] as dependencies, it'll work.
You should remove stateReady from your dependency array in the useEffect hook. Adding variables in the dependency array means that the use Effect hooks fires only when one of the dependencies changes. Here's how to use useEffect as lifecycle methods https://reactjs.org/docs/hooks-effect.html
useEffect(() => {
console.log('is fetching', isFetching)
if(comments.length === 0) {
getAllComments();
}
});

To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function

I have this code
import ReactDOM from "react-dom";
import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
function ParamsExample() {
return (
<Router>
<div>
<h2>Accounts</h2>
<Link to="/">Netflix</Link>
<Route path="/" component={Miliko} />
</div>
</Router>
);
}
const Miliko = ({ match }) => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
(async function() {
setIsError(false);
setIsLoading(true);
try {
const Res = await fetch("https://foo0022.firebaseio.com/New.json");
const ResObj = await Res.json();
const ResArr = await Object.values(ResObj).flat();
setData(ResArr);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
})();
console.log(data);
}, [match]);
return <div>{`${isLoading}${isError}`}</div>;
};
function App() {
return (
<div className="App">
<ParamsExample />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I created three links that open the Miliko component. but when I quickly click on the links I get this error:
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
I think the problem is caused by dismount before async call finished.
const useAsync = () => {
const [data, setData] = useState(null)
const mountedRef = useRef(true)
const execute = useCallback(() => {
setLoading(true)
return asyncFunc()
.then(res => {
if (!mountedRef.current) return null
setData(res)
return res
})
}, [])
useEffect(() => {
return () => {
mountedRef.current = false
}
}, [])
}
mountedRef is used here to indicate if the component is still mounted. And if so, continue the async call to update component state, otherwise, skip them.
This should be the main reason to not end up with a memory leak (access cleanedup memory) issue.
Demo
https://codepen.io/windmaomao/pen/jOLaOxO , fetch with useAsync
https://codepen.io/windmaomao/pen/GRvOgoa , manual fetch with useAsync
Update
The above answer leads to the following component that we use inside our team.
/**
* A hook to fetch async data.
* #class useAsync
* #borrows useAsyncObject
* #param {object} _ props
* #param {async} _.asyncFunc Promise like async function
* #param {bool} _.immediate=false Invoke the function immediately
* #param {object} _.funcParams Function initial parameters
* #param {object} _.initialData Initial data
* #returns {useAsyncObject} Async object
* #example
* const { execute, loading, data, error } = useAync({
* asyncFunc: async () => { return 'data' },
* immediate: false,
* funcParams: { data: '1' },
* initialData: 'Hello'
* })
*/
const useAsync = (props = initialProps) => {
const {
asyncFunc, immediate, funcParams, initialData
} = {
...initialProps,
...props
}
const [loading, setLoading] = useState(immediate)
const [data, setData] = useState(initialData)
const [error, setError] = useState(null)
const mountedRef = useRef(true)
const execute = useCallback(params => {
setLoading(true)
return asyncFunc({ ...funcParams, ...params })
.then(res => {
if (!mountedRef.current) return null
setData(res)
setError(null)
setLoading(false)
return res
})
.catch(err => {
if (!mountedRef.current) return null
setError(err)
setLoading(false)
throw err
})
}, [asyncFunc, funcParams])
useEffect(() => {
if (immediate) {
execute(funcParams)
}
return () => {
mountedRef.current = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return {
execute,
loading,
data,
error
}
}
Update 2022
This approach has been adopted in the book https://www.amazon.com/Designing-React-Hooks-Right-Way/dp/1803235950 where this topic has been mentioned in useRef and custom hooks chapters, and more examples are provided there.
useEffect will try to keep communications with your data-fetching procedure even while the component has unmounted. Since this is an anti-pattern and exposes your application to memory leakage, cancelling the subscription to useEffect optimizes your app.
In the simple implementation example below, you'd use a flag (isSubscribed) to determine when to cancel your subscription. At the end of the effect, you'd make a call to clean up.
export const useUserData = () => {
const initialState = {
user: {},
error: null
}
const [state, setState] = useState(initialState);
useEffect(() => {
// clean up controller
let isSubscribed = true;
// Try to communicate with sever API
fetch(SERVER_URI)
.then(response => response.json())
.then(data => isSubscribed ? setState(prevState => ({
...prevState, user: data
})) : null)
.catch(error => {
if (isSubscribed) {
setState(prevState => ({
...prevState,
error
}));
}
})
// cancel subscription to useEffect
return () => (isSubscribed = false)
}, []);
return state
}
You can read up more from this blog juliangaramendy
Without #windmaomao answer, I could spend other hours trying to figure out how to cancel the subscription.
In short, I used two hooks respectively useCallback to memoize function and useEffect to fetch data.
const fetchSpecificItem = useCallback(async ({ itemId }) => {
try {
... fetch data
/*
Before you setState ensure the component is mounted
otherwise, return null and don't allow to unmounted component.
*/
if (!mountedRef.current) return null;
/*
if the component is mounted feel free to setState
*/
} catch (error) {
... handle errors
}
}, [mountedRef]) // add variable as dependency
I used useEffect to fetch data.
I could not call the function inside effect simply because hooks can not be called inside a function.
useEffect(() => {
fetchSpecificItem(input);
return () => {
mountedRef.current = false; // clean up function
};
}, [input, fetchSpecificItem]); // add function as dependency
Thanks, everyone your contribution helped me to learn more about the usage of hooks.
fetchData is an async function which will return a promise. But you have invoked it without resolving it. If you need to do any cleanup at component unmount, return a function inside the effect that has your cleanup code. Try this :
const Miliko = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState('http://hn.algolia.com/api/v1/search?query=redux');
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
(async function() {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
})();
return function() {
/**
* Add cleanup code here
*/
};
}, [url]);
return [{ data, isLoading, isError }, setUrl];
};
I would suggest reading the official docs where it is clearly explained along with some more configurable parameters.
Folowing #Niyongabo solution, the way I ended up that fixed it was:
const mountedRef = useRef(true);
const fetchSpecificItem = useCallback(async () => {
try {
const ref = await db
.collection('redeems')
.where('rewardItem.id', '==', reward.id)
.get();
const data = ref.docs.map(doc => ({ id: doc.id, ...doc.data() }));
if (!mountedRef.current) return null;
setRedeems(data);
setIsFetching(false);
} catch (error) {
console.log(error);
}
}, [mountedRef]);
useEffect(() => {
fetchSpecificItem();
return () => {
mountedRef.current = false;
};
}, [fetchSpecificItem]);
Create a mutable ref object and set it to true, and during clean-up toggle its value, to ensure that the component has been unmouted.
const mountedRef = useRef(true)
useEffect(() => {
// CALL YOUR API OR ASYNC FUNCTION HERE
return () => { mountedRef.current = false }
}, [])
const [getAllJobs, setgetAlljobs] = useState();
useEffect(() => {
let mounted = true;
axios.get('apiUrl')
.then(function (response) {
const jobData = response.data;
if (mounted) {
setgetAlljobs(jobData)
}
})
.catch(function (error) {
console.log(error.message)
})
return () => mounted = false;
}, [])
set a variable mounted to true->
then if it is true, mount the function->
in the bottom you return it to unmount it
My case was pretty different from what this questions wants. Still I got the same error.
My case was because I had a 'list', which was rendered by using .map from array. And I needed to use .shift. (to remove first item in array)
If array had just one item, it was ok, but since it had 2 of them -> the first one got 'deleted/shifted' and because I used key={index} (while index was from .map), it assumed, that the second item, which later was first, was the same component as the shifted item..
React kept info from the first item (they were all nodes) and so, if that second node used useEffect(), React threw error, that the component is already dismounted, because the former node with index 0 and key 0 had the same key 0 as the second component.
The second component correctly used useEffect, but React assumed, that it is called by that former node, which was no longer on the scene -> resulting in error.
I fixed this by adding different key prop value (not index), but some unique string.
you can wrap any action as a callback inside checkUnmount
const useUnmounted = () => {
const mountedRef = useRef(true);
useEffect(
() => () => {
mountedRef.current = false;
},
[],
);
const checkUnmount = useCallback(
(cb = () => {}) => {
try {
if (!mountedRef.current) throw new Error('Component is unmounted');
cb();
} catch (error) {
console.log({ error });
}
},
[mountedRef.current],
);
return [checkUnmount, mountedRef.current];
};
import React, { useCallback, useEffect, useRef, useState } from "react";
import { userLoginSuccessAction } from "../../../redux/user-redux/actionCreator";
import { IUser } from "../../../models/user";
import { Navigate } from "react-router";
import XTextField from "../../../x-lib/x-components/x-form-controls/XTextField";
import { useDispatch } from "react-redux";
interface Props {
onViewChange?: (n: number) => void;
userInit?: (user: IUser) => void;
}
interface State {
email: string;
password: string;
hasError?: boolean;
errorMessage?: string;
}
const initialValue = {
email: "eve.holt#reqres.in",
password: "cityslicka",
errorMessage: "",
};
const LoginView: React.FC<Props> = (props) => {
const { onViewChange } = props;
const [state, setState] = useState(initialValue);
const mountedRef = useRef(true);
const dispatch = useDispatch();
const handleEmailChange = useCallback(
(val: string) => {
setState((state) => ({
...state,
email: val,
}));
},
[state.email]
);
const handlePasswordChange = useCallback(
(val: string) => {
setState((state) => ({
...state,
password: val,
}));
},
[state.password]
);
const onUserClick = useCallback( async () => {
// HTTP Call
const data = {email: state.email , password: state.password}
try{
await dispatch(userLoginSuccessAction(data));
<Navigate to = '/' />
setState( (state)=>({
...state,
email: "",
password: ""
}))
}
catch(err){
setState( (state)=>({
...state,
errorMessage: err as string
}))
}
},[mountedRef] )
useEffect(()=>{
onUserClick();
return ()=> {
mountedRef.current = false;
};
},[onUserClick]);
const Error = (): JSX.Element => {
return (
<div
className="alert alert-danger"
role="alert"
style={{ width: "516px", margin: "20px auto 0 auto" }}
>
{state.errorMessage}
</div>
);
};
return (
<div>
<div>
email: "eve.holt#reqres.in"
<span style={{ paddingRight: "20px" }}></span> password: "cityslicka"{" "}
</div>
{state.errorMessage && <Error />}
<form className="form-inline">
<div className="form-group">
<XTextField
label="email"
placeholder="E-Posta"
value={state.email}
onChange={handleEmailChange}
/>
</div>
<div className="form-group my-sm-3">
<XTextField
type="password"
label="password"
placeholder="Şifre"
value={state.password}
onChange={handlePasswordChange}
/>
</div>
<button type="button" className="btn btn-primary" onClick = {onUserClick} >
Giriş Et
</button>
<a
href="#"
onClick={(e) => {
e.preventDefault();
onViewChange && onViewChange(3);
}}
>
Şifremi Unuttum!
</a>
</form>
<p>
Hələdə üye deyilsiniz? <br />
pulsuz registir olmak üçün
<b>
<u>
<a
style={{ fontSize: "18px" }}
href="#"
onClick={(e) => {
e.preventDefault();
onViewChange && onViewChange(2);
}}
>
kilik edin.
</a>
</u>
</b>
</p>
</div>
);
};
export default LoginView;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
For this problem I used a tricky way
first I deploy a state like this
const [routing,setRouting] = useState(false)
then when my works finished I changed it to true
and change my useEffect like this
useEffect(()=>{
if(routing)
navigation.navigate('AnotherPage')
),[routing]}

Resources