why useEffect is called infinitely - javascript

In useEffect in my react component I get data and I update a state, but I don't know why the useEffect is always executed:
const Comp1 = () => {
const [studies, setStudies]= useState([]);
React.useEffect( async()=>{
await axios.get('/api/expert/',
)
.then((response) => {
setStudies(response.data.studies);
}, (error) => {
console.log(error );
})
console.log("called+ "+ studies);
},[studies]);
return(
<Comp2 studies={studies}/>
)
}
Here is my second Component used in the first component...
const Comp2 = (props) => {
const [studies, setStudies]= useState([]);
React.useEffect( ()=>{
setStudies(props.studies)
},[props.studies, studies]);
return(
studies.map((study)=>{console.log(study)})
}

EDIT
const Comp2 = (props) => {
// for some brief time props.studies will be an empty array, []
// you need to decide what to do while props.studies is empty.
// you can show some loading message, show some loading status,
// show an empty list, do whatever you want to indicate
// progress, dont anxious out your users
return (
props.studies.map((study)=>{console.log(study)}
)
}
You useEffect hook depends on the updates that the state studies receive. Inside this useEffect hook you update studies. Can you see that the useEffect triggers itself?
A updates B. A runs whenever B is updated. (goes on forever)
How I'd do it?
const Comp1 = () => {
const [studies, setStudies]= useState([]);
React.useEffect(() => {
const asyncCall = async () => {
await axios.get('/api/expert/',
)
.then((response) => {
setStudies(response.data.studies);
}, (error) => {
console.log(error );
})
console.log("called+ "+ studies);
}
asyncCall();
}, []);
return(
<Comp2 studies={studies}/>
)
}

useEffect() has dependency array which causes it to execute if any value within it updates. Here, setStudies updates studies which is provided as dependency array and causes it to run again and so on. To prevent this, remove studies from the dependency array.
Refer: How the useEffect Hook Works (with Examples)

Related

Props defined by async function by Parent in UseEffect passed to a child component don't persist during its UseEffect's clean-up

Please consider the following code:
Parent:
const Messages = (props) => {
const [targetUserId, setTargetUserId] = useState(null);
const [currentChat, setCurrentChat] = useState(null);
useEffect(() => {
const { userId } = props;
const initiateChat = async (targetUser) => {
const chatroom = `${
userId < targetUser
? `${userId}_${targetUser}`
: `${targetUser}_${userId}`
}`;
const chatsRef = doc(database, 'chats', chatroom);
const docSnap = await getDoc(chatsRef);
if (docSnap.exists()) {
setCurrentChat(chatroom);
} else {
await setDoc(chatsRef, { empty: true });
}
};
if (props.location.targetUser) {
initiateChat(props.location.targetUser.userId);
setTargetUserId(props.location.targetUser.userId);
}
}, [props]);
return (
...
<Chat currentChat={currentChat} />
...
);
};
Child:
const Chat = (props) => {
const {currentChat} = props;
useEffect(() => {
const unsubscribeFromChat = () => {
try {
onSnapshot(
collection(database, 'chats', currentChat, 'messages'),
(snapshot) => {
// ... //
}
);
} catch (error) {
console.log(error);
}
};
return () => {
unsubscribeFromChat();
};
}, []);
...
The issue I'm dealing with is that Child's UseEffect clean up function, which depends on the chatroom prop passed from its parent, throws a TypeError error because apparently chatroom is null. Namely, it becomes null when the parent component unmounts, the component works just fine while it's mounted and props are recognized properly.
I've tried different approaches to fix this. The only way I could make this work if when I moved child component's useEffect into the parent component and defined currentChat using useRef() which honestly isn't ideal.
Why is this happening? Shouldn't useEffect clean-up function depend on previous state? Is there a proper way to fix this?
currentChat is a dependency of that effect. If it's null, the the unsubscribe should just early return.
const {currentChat} = props;
useEffect(() => {
const unsubscribeFromChat = () => {
if(!currentChat) return;
try {
onSnapshot(
collection(database, 'chats', currentChat, 'messages'),
(snapshot) => {
// ... //
}
);
} catch (error) {
console.log(error);
}
};
return () => {
unsubscribeFromChat();
};
}, [currentChat]);
But that doesn't smell like the best solution. I think you should handle all the subscribing/unsubscribing in the same component. You shouldn't subscribe in the parent and then unsubscribe in the child.
EDIT:
Ah, there's a bunch of stuff going on here that's not good. You've got your userId coming in from props - props.location.targetUser.userId and then you're setting it as state. It's NOT state, it's only a prop. State is something a component owns, some data that a component has created, some data that emanates from that component, that component is it's source of truth (you get the idea). If your component didn't create it (like userId which is coming in on props via the location.targetUser object) then it's not state. Trying to keep the prop in sync with state and worry about all the edge cases is a fruitless exercise. It's just not state.
Also, it's a codesmell to have [props] as a dependency of an effect. You should split out the pieces of props that that effect actually needs to detect changes in and put them in the dependency array individually.

What is the sequence of actions when using promises in useEffect hook in React?

In the code below I was expecting that inside the useEffect hook console.log procedure will log the list of the tickets fetched by getAllTickets function because list of the tickets is returned without error.However console.log results in logging an empty array that I set up in the state initially. It seems that console.log is somehow preceding the fetch process and following setTickets hook. Could someone help to clarify this confusion?
import ReactDOM from 'react-dom';
import * as React from 'react';
import { getAllTickets } from './api';
const App = () => {
const [tickets, setTickets] = React.useState([]);
React.useEffect(() => {
getAllTickets()
.then((res) => setTickets([...res]))
.then(() => console.log(tickets))
.catch((e) => console.log(e));
}, []);
const renderTickets = tickets.map((t) => (
<div key={t.id}>
<p>{t.description}</p>
</div>
));
return (
<div>
{renderTickets}
<button onClick={() => setOpen(true)}>Add ticket</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
getAllTickets fetches list of tickets from db as below
export const getAllTickets = async () => {
const res = await fetch('http://localhost:3001/tickets');
const data = await res.json();
console.log(data);
return data;
};
It's not that there's a sequencing issue. It's that the function logging the tickets value is logging an old value.
Your useEffect callback is created each time your component function is called, but because your dependency array is empty, only the first of those functions is ever called by useEffect. That first function closes over the tickets constant for the call to your component function that created the callback (the first call), in which tickets is an empty array. So although setTickets updates your state item, which will cause your component function to be called again, your useEffect callback (or more accurately, the callbacks it creates) continue to use the old value.
If you want to see the updated array, you can use res:
React.useEffect(() => {
getAllTickets()
.then((res) => {
const ticketsReceived = [...res]; // Why the shallow copy?
setTickets(ticketsReceived);
console.log(ticketsReceived);
})
.catch((e) => console.log(e));
}, []);

React Asynchronous Fetching

Using React and React-Dom CDN 16
I am new to React and trying to build a dashboard component that takes the value of one of three buttons in a Buttons component and sends the value to a List component. The List component fetches data from an API and renders the results.
The feature works fine up until the data fetching, which it only does once the app is rendered the first time. I've logged that the state that's set by the Buttons component is making its way to the List component and the fetch action is updating dynamically correctly, but the fetching functionality isn't getting triggered when that state updates.
Here's the code.
const { useState, useEffect } = React
const App = props => {
return (
<div className="app-content">
<Dashboard />
</div>
);
};
const Dashboard = props => {
const [timespan, setTimespan] = useState('week');
const changeTime = time => setTimespan(time);
return(
<div>
<p>{timespan}</p> // this state updates when the buttons are clicked
<Buttons onUpdate={changeTime} />
<List timespan={timespan}/>
</div>
);
};
const Buttons = props => {
return (
<div>
<button onClick={props.onUpdate.bind( this, 'week' )}>
week
</button>
<button onClick={props.onUpdate.bind( this, 'month' )}>
month
</button>
<button onClick={props.onUpdate.bind( this, 'all' )}>
all
</button>
</div>
);
};
const List = props => {
const timespan = props.timespan;
const homepage = `${location.protocol}//${window.location.hostname}`;
const action = `${homepage}?fetchDataset=1&timespan=${timespan}`;
// if I console.log(action) here the URL is updated correctly
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [obj, setObj] = useState([]);
useEffect(() => {
fetch(action)
.then(res => res.json())
.then(
(result) => { // if I console.log(result) here I only get a response at initialization
setIsLoaded(true);
setObj(result);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
}, []);
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<div>
// my API returns "timespan is: $timespan", but this is only ever "week" because that's the initial state of the timespan
{obj}
</div>
);
};
};
ReactDOM.render(
<App />,
document.getElementById('app')
);
I think I must be overlooking something very obvious because this seems like one of the core purposes of React, but it's hard to find documentation that is relevant with version 16 updates like function classes and hooks.
I really appreciate any help. Thanks!
you need to add timeSpan (or action) to your useEffect dependency array:
useEffect(() => {
fetch(action)
.then(res => res.json())
.then(
result => {
setIsLoaded(true);
setObj(result);
},
error => {
setIsLoaded(true);
setError(error);
}
);
}, [timeSpan]); // [action] will also solve this
This way the effect will know it needs to run every time the timeSpan prop changes.
By passing an empty dependency array you are telling the effect to only run once - when the component it mounted.

UseEffect is empty in the beginning

I m traying to create a component using Hooks.
Inside useEffect I call a function, but it dosen 't work. I think is because when the app starts the props is empty and in the second, when is full dosen 't call the function again..
WHY I m doing wrong?
Thanks (sorry for my bad english)
this is the code:
import React, { useState, useEffect } from "react";
const fetchContent = async content => {
console.log(content) // is empty in the first console, and full in the second.
const data = [];
for await (const item of content) {
data.push({ componentName: item.nombre});
}
return data;
};
function ContentGroups(props) {
const [contentResult, setResult] = useState([]);
useEffect(() => {
fetchContent(props.info).then(data => setResult(data));
console.log(contentResult) // is empty in the first console, and full in the second.
}, []);
return (
<React.Fragment>
{contentResult.map((el, index) => {
switch (el.componentName) {
case "top":
return <h1>soy un top words</h1>/*
}
})}
</React.Fragment>
);
}
Try adding a props dependency in your useEffect deps array:
useEffect(() => {
fetchContent(props.info).then(data => setResult(data));
}, [props.info]);
Why your example not working?
First, you logging your data while setState is async, console.log(contentResult) will log the current state before the update.
Secondly, you run your useEffect with empty dep array which means you run it once on the component mount.
console.log prints before getting the result
useEffect(() => {
fetchContent(props.info).then(data => {
setResult(data);
console.log(data);
});
}, []);

useEffect spamming requests

State
const [user, setUser] = useState({});
checkIfUserIsEnabled()
async function checkIfUserIsEnabled() {
const res = await fetch("http://localhost:8080/users/finduserbytoken?id=" +
getTokenIdFromURL);
res.json()
.then(res => setUser(res))
.catch(err => setErrors(err));
}
useEffect When i call my checkIfUserIsEnabled() in the useEffect below it gets rendered once and displays the false version in the return method.
useEffect(() => {
verifyEmail(getTokenIdFromURL);
checkIfUserIsEnabled();
return () => {
/* cleanup */
};
}, [/* input */])`
useEffect (2th) If i do it like this instead, it keeps spamming the requests towards my API and displays true.
useEffect(() => {
checkIfUserIsEnabled();
});
Return
return (
<div className="emailVerificationWrapper">
{user.enabled
? <h1>Thank you for registrating, {user.firstName}. Account is verified!</h1>
: <h1>Attempting to verify account...</h1>}
</div>
)
To my question(s): Why does the second useEffect spam the request? and is there a way i can make the request being rendered every ~2-3 second instead of the spam? and could i make it stop doing the request once it actually returns true?
The effect hook runs when the component mounts but also when the component updates. Because we are setting the state after every data fetch, the component updates and the effect runs again.
It fetches the data again and again. That's a bug and needs to be avoided. We only want to fetch data when the component mounts. That's why you can provide an empty array(or something which doesn't change) as second argument to the effect hook to avoid activating it on component updates(or only when that parameter changes) but only for the mounting of the component.
let URL = `http://localhost:8080/users/finduserbytoken?id=`;
async function checkIfUserIsEnabled() {
const res = await fetch(`$(URL)` +
getTokenIdFromURL);
res.json()
.then(res => {setUser(res); return Promise.resolve()})
.catch(err => {setErrors(err); return Promise.reject()});
}
useEffect(() => {
const abortController = new AbortController();
const fetchData = async() => await checkIfUserIsEnabled();
fetchData();
return () => {
abortController.abort();
};
}, [URL]);
useEffect(() => {
checkIfUserIsEnabled();
}); <-- no dependency
As your useEffect doesn't have any dependency it will run on every render, so every time you change some state and your component re-renders it will send requests.

Categories

Resources