React- why useEffect called many times (even with condition)? - javascript

I work on a simple chat application with react hooks. In the second useEffect I need to append the new message to the rest of of them so I will be able to display all of them in the chat. Now i able to append the message only after the useEffect is being called x2 from the array's length. For example: in the forth message the UseEffect will be execute 8 times before the array will be complete.
notice: in the useEffect I setAllMessages twice but only one is being execute which is fine depends if it is a reciever of sender (so I dont think this the problem)
function Chat() {
const [message, setMessage] = useState("");
const [userName] = useState(
JSON.parse(atob(localStorage.getItem("token").split(".")[1])).name
);
const [userTyping, setUserTyping] = useState(null);
const [allMessages, setAllMessages] = useState([]);
useEffect(() => {
socket.emit("join", userName);
socket.on("chat-message", data => {
toast(`Hello ${data}`);
});
socket.on("user-joined", data => {
toast(`${data} joined the chat`);
});
}, [userName]);
useEffect(() => { // the problem is here
function handleAllMessages(data) {
setAllMessages([...allMessages, data]);
console.log(allMessages);
}
socket.on("broadcast-message", data => {
handleAllMessages(data);
});
socket.on("my-message", data => {
data["userName"] = "You";
handleAllMessages(data);
});
}, [allMessages]);
useEffect(() => {
socket.on("who-typing", data => {
setUserTyping(data);
setTimeout(() => {
setUserTyping(null);
}, 2500);
});
}, [userTyping]);
useEffect(() => {
socket.on("user-disconnected", data => {
toast(`${data} left the chat`);
});
}, []);
function handleChat(e) {
e.preventDefault();
if (message.trim() === "") return toast.warn("not valid message");
socket.emit("user-message", message, userName);
setMessage("");
}
function handleChange(e) {
setMessage(e.target.value);
socket.emit("typing", userName);
}
return (...

The reason while the second useEffect will be executed multiple times -in face I suppose infinitely-, is because you are telling it to execute each time allMessages change, since it's in the Array -the second argument for useEffect.
Passing that array with allMessages in it will cause the useEffect to re-run every time allMessages change, which is actually what you are doing inside the useEffect when you are calling the handleAllMessages function.
Read about this.

In hooks the right way to setAllMessages (in this case) is to pass the oldArray like this...
setAllMessages(allMessages => [...allMessages, data]);
and inside the use effect pass empty array as a second argument instead [allMessages]
this fix the problem

Related

React: How to update a state prior to another function?

I have a state variable called list that updates when setList is called. SetList lives under the function AddToList, which adds a value to the existing values in the list. As of this moment, the function handleList executes prior to the state variable setList even though I have setList added prior to the function handleList. What I am trying to achieve is for the setList to update its list prior to running the handleList. Could you provide insights on how to fix this?
If you want to test the code, https://codesandbox.io/s/asynchronous-test-mp2fq?file=/Form.js
export default function Form() {
const [list, setList] = useState([]);
const addToList = (name) => {
let newDataList = list.concat(name);
setList(newDataList);
console.log("List: ", list);
handleList();
};
const handleList = async () => {
console.log("Handle List Triggered");
await axios
// .put("", list)
.get("https://api.publicapis.org/entries")
.then((response) => {
console.log("Response: ", response);
})
.catch((error) => {});
};
return (
<AutoComplete
name="list"
label="Add to List"
onChange={(events, values) => {
addToList(values.title);
}}
/>
);
}
As you can tell, the get response is made prior to updating the list.
It's not clear what you want to do with the updated list, but you know what the new list will be, so you can just pass that around if you need it immediately.
const addToList = (name) => {
let newDataList = list.concat(name);
setList(newDataList);
console.log("List: ", list);
handleList(newDataList);
};
const handleList = async (list) => {
console.log("Handle List Triggered");
await axios
// .put("", list)
.get("https://api.publicapis.org/entries")
.then((response) => {
console.log("Response: ", response);
})
.catch((error) => {});
};
React's useEffect hook has an array of dependencies that it watches for changes. While a lot of the time it's used with no dependencies (i.e. no second parameter or empty array) to replicate the functionality of componentDidMount and componentDidUpdate, you may be able to use that to trigger handleList by specifying list as a dependency like this:
useEffect(() => {
handleList(list);
}, [list]);
I think there may be a redundant request when page loads though because list will be populated which you'll most likely want to account for to prevent unnecessary requests.
first of all you have to understand setState is not synchronized that means when you call setList(newDataList) that not gonna triggered refer why setState is async
therefore you can use #spender solution
or useStateCallback hook but it's important understand setState is not sync
const [state, setState] = useStateCallback([]);
const addToList = (name) => {
........... your code
setList(newDataList, () => {
// call handleList function here
handleList();
});
}

Refresh data received from an API every minute React, Javascript

I am working on a little project that involves getting the weather at different locations and also, dynamically getting the time at those locations afterwards. All these worked fine but then i tried to take it a step further by attempting to refresh the data gotten from the API every minute(without resubmitting/pressing the enter key).
I've tried various ways of implementing the setinterval function into the code but none seem to work. Here is what the code looks like:
function App() {
const [query, setQuery] = useState("");
const [weather, setWeather] = useState({});
const [timeZone, setTimeZone] = useState({});
const handleChange = (e) => {
setQuery(e.target.value);
};
const apiCall = () => {
Axios({
method: "get",
url: `${api.timeBase}apiKey=${api.timeKey}&location=${query}`,
timeout: 10000,
})
.then((res) => {
console.log(res.data);
setWeather(res.data);
//setQuery("");
})
.catch((err) => {
console.log(err);
});
};
const handleSubmit = (e) => {
e.preventDefault();
apiCall();
};
useEffect(() => {
setInterval(apiCall, 60000);
}, []);
useEffect(() => {
if (weather.main !== undefined) {
Axios.get(
`${api.timeBase}apiKey=${api.timeKey}&location=${weather.name}, ${weather.sys.country}`
)
.then((res) => {
console.log(res.data);
setTimeZone(res.data);
})
.catch((err) => {
console.log(err);
});
}
}, [weather]);
Basically, the issue i get the most is that somewhere within the setinterval function, the queryparameter is cleared and the API continues to retrieve a different location till gets back to normal. i tried preventing the query parameter from clearing after it updates the state, that didnt help.
PS: i had to breakdown the handlesubmit because attaching the setinterval to the original block of code throws an error cause e.preventDefault isn't defined. Also, I think calling the setInterval without the useEffect makes things a lot worse too.
Clear out the interval after the interval is finished. The setInterval function returns a timerId that should be cleared out otherwise, it remains in the timer pool and can execute even after the instance of the callback has been run.
React.useEffect(() => {
const id = setInterval(apiCall, 60000);
return () => clearInterval(id);
}, []);

React : Updating and then accessing state after a network request in useEffect hook. State remains stale

Im trying to update and reference hasError state field inside of the initialization function of my component in order to control if a redirect happens after successful initialization or if error gets displayed.
Here is a condensed version of the issue:
const [hasError, setHasError] = useState(false);
useEffect(() => {
initialize();
}, []);
async function initialize(){
try {
await networkRequest();
} catch (err) {
setHasError(true);
}
console.log(hasError); // <- is still false
if(!hasError){
redirect() // <- causes redirect even with error
}
}
function networkRequest() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject();
}, 1000);
});
}
The initialization function should only be called once on component mount which is why I'm passing [] to useEffect. Passing [hasError] to useEffect also doesn't make sense since I don't want initialization to run everytime hasError updates.
I have seen people recommend using useReducer but that seems hacky since I'm already using Redux on this component and I'll need to use 2 different dispatch instances then.
How is a use case like this typically handled?
You will have to create another useEffect hook that "listens" for changes to hasError. setHasError in this case is asynchronous, so the new value won't be available to you immediately.
I don't know what the rest of your component looks like, but it sounds like you should have some sort of isLoading state that will be used to display a loading message, then once your request finishes and it fails, you render what you need, or if it succeeds, you redirect.
Here's an example:
function App() {
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
useEffect(() => {
(async () => {
try {
await networkRequest();
isLoading(false);
} catch (error) {
setHasError(error);
isLoading(false);
}
})()
}, [])
useEffect(() => {
if (!isLoading && !hasError) {
redirect();
}
}, [isLoading, hasError]);
if (isLoading) { return "Loading"; }
// else, do whatever you need to do here
}

my useEffect is reaching callstack and after text change, i don't know why?

i want to fire a callback after a text change, basically this is for search. My code:
const fetchMovies = useCallback(async () => {
console.log('fetchMovies api ');
const {Search} = await fetch(
`http://www.omdbapi.com/?apikey=${apiKey}&s=${text}&page=${page}`,
).then(data => data.json());
console.log('movies', Search);
return Search;
}, [page, text]);
useEffect(() => {
console.log('useEffect!');
if (timeout) {
clearTimeout(timeout);
}
if (text) {
const newTimeout = setTimeout(async () => {
dispatch(fetchMoviesRequest('fetch'));
console.log('fetch!1');
try {
const moviesResult = await fetchMovies();
console.log('fetch!2', moviesResult);
dispatch(fetchMoviesSuccess(moviesResult));
} catch (fetchError) {
console.log('fetch!3e', fetchError);
dispatch(fetchMoviesFailure(fetchError));
}
}, 2000);
dispatch(onSetTimeout(newTimeout));
}
return () => {
clearTimeout(timeout);
};
}, [fetchMovies, text, timeout, page]);
somehow i cannot figure out what causes it to rerender so much when it's supposed to rerender only after text change? i can only type 1 letter and app crashes with error of max call stack?
I'm not sure what the values of your other variables are in your useEffect dependency array, but what looks suspicious to me is that you have timeout as one of the dependencies.
I'm going on a hunch and say that this line onSetTimeout(newTimeout) will change the value of the timeout variable which will re-trigger this useEffect. This will create an infinite loop because the effect will run every time timeout changes.
Have you tried changing your useEffect's dependency list to only [text]?
I'm not too sure, but I think that'll only call the function if text changes.

Setting a useEffect hook's dependency within`useEffect` without triggering useEffect

Edit: It just occurred to me that there's likely no need to reset the variable within the useEffect hook. In fact, stateTheCausesUseEffectToBeInvoked's actual value is likely inconsequential. It is, for all intents and purposes, simply a way of triggering useEffect.
Let's say I have a functional React component whose state I initialize using the useEffect hook. I make a call to a service. I retrieve some data. I commit that data to state. Cool. Now, let's say I, at a later time, interact with the same service, except that this time, rather than simply retrieving a list of results, I CREATE or DELETE a single result item, thus modifying the entire result set. I now wish to retrieve an updated copy of the list of data I retrieved earlier. At this point, I'd like to again trigger the useEffect hook I used to initialize my component's state, because I want to re-render the list, this time accounting for the newly-created result item.
​
const myComponent = () => {
const [items, setItems] = ([])
useEffect(() => {
const getSomeData = async () => {
try {
const response = await callToSomeService()
setItems(response.data)
setStateThatCausesUseEffectToBeInvoked(false)
} catch (error) {
// Handle error
console.log(error)
}
}
}, [stateThatCausesUseEffectToBeInvoked])
const createNewItem = async () => {
try {
const response = await callToSomeService()
setStateThatCausesUseEffectToBeInvoked(true)
} catch (error) {
// Handle error
console.log(error)
}
}
}
​
I hope the above makes sense.
​
The thing is that I want to reset stateThatCausesUseEffectToBeInvoked to false WITHOUT forcing a re-render. (Currently, I end up calling the service twice--once for win stateThatCausesUseEffectToBeInvoked is set to true then again when it is reset to false within the context of the useEffect hook. This variable exists solely for the purpose of triggering useEffect and sparing me the need to elsewhere make the selfsame service request that I make within useEffect.
​
Does anyone know how this might be accomplished?
There are a few things you could do to achieve a behavior similar to what you described:
Change stateThatCausesUseEffectToBeInvoked to a number
If you change stateThatCausesUseEffectToBeInvoked to a number, you don't need to reset it after use and can just keep incrementing it to trigger the effect.
useEffect(() => {
// ...
}, [stateThatCausesUseEffectToBeInvoked]);
setStateThatCausesUseEffectToBeInvoked(n => n+1); // Trigger useEffect
Add a condition to the useEffect
Instead of actually changing any logic outside, you could just adjust your useEffect-body to only run if stateThatCausesUseEffectToBeInvoked is true.
This will still trigger the useEffect but jump right out and not cause any unnecessary requests or rerenders.
useEffect(() => {
if (stateThatCausesUseEffectToBeInvoked === true) {
// ...
}
}, [stateThatCausesUseEffectToBeInvoked]);
Assuming that 1) by const [items, setItems] = ([]) you mean const [items, setItems] = useState([]), and 2) that you simply want to reflect the latest data after a call to the API:
When the state of the component is updated, it re-renders on it's own. No need for stateThatCausesUseEffectToBeInvoked:
const myComponent = () => {
const [ items, setItems ] = useState( [] )
const getSomeData = async () => {
try {
const response = await callToSomeService1()
// When response (data) is received, state is updated (setItems)
// When state is updated, the component re-renders on its own
setItems( response.data )
} catch ( error ) {
console.log( error )
}
}
useEffect( () => {
// Call the GET function once ititially, to populate the state (items)
getSomeData()
// use [] to run this only on component mount (initially)
}, [] )
const createNewItem = async () => {
try {
const response = await callToSomeService2()
// Call the POST function to create the item
// When response is received (e.g. is OK), call the GET function
// to ask for all items again.
getSomeData()
} catch ( error ) {
console.log( error )
}
} }
However, instead of getting all items after every action, you could change your array locally, so if the create (POST) response.data is the newly created item, you can add it to items (create a new array that includes it).

Categories