React axios get request without resulting in infinite loop using useEffect - reactjs

I'm getting an infinite loop with this axios get request, but when I try putting messages or setMessages as the value inside of the [] at the end of useEffect it takes 2 clicks of the button to update the screen or it results in an error saying insufficient network resources. So what should I put instead?
function CommentsPage() {
const { user } = useAuth0();
const [query, setQuery] = useState("");
const [messages, setMessages] = useState([]);
//fetches data from mongodb for comments
useEffect(() => {
fetchComments();
}, [messages]);
//calls backend to fetch data from the messages collection on mongodb
async function fetchComments() {
const response = await axios.get("http://localhost:5000/messages");
setMessages(response.data);
}
// handles when a user types in the comment bar
function handleOnInputChange(event) {
// current value of what user is typing
const { value } = event.target;
setQuery(value);
}
// handles when a user posts a comment
function postComment(comment, user) {
const newMessage = {
username: user.name,
content: comment,
};
// calls backend to send a post request to insert a new document into the collection
axios
.post("http://localhost:5000/messages", newMessage)
.then((res) => console.log(res.data));
setQuery("");
fetchComments();
}
// handles when a user deletes a comment
function deleteComment(id) {
axios
.delete("http://localhost:5000/messages/delete/" + id)
.then((res) => console.log(res.data));
fetchComments();
setMessages(messages.filter((message) => message.id !== id));
}
// handles when a user updates a comment
function updateComment(id) {
// calls a pop up that allows user to input their new comment
const editedContent = prompt("please enter new message");
const newMessage = {
username: user.name,
content: editedContent,
};
axios
.put("http://localhost:5000/messages/update/" + id, newMessage)
.then((res) => console.log(res.data));
fetchComments();
}
console.log(messages);
return (
<Container>
<CommentsContainer>
{messages.length ? (
messages.map((message) => {
{/* if the usernames match then a user comment is returned
otherwise an other comment will be returned */}
return message.username === user.name ? (
<UserComment
key={message._id}
username={message.username}
content={message.content}
deleteComment={deleteComment}
id={message._id}
updateComment={updateComment}
/>
) : (
<OtherComment
key={message._id}
username={message.username}
content={message.content}
/>
);
})
) : (
<div>There are no comments. Make one!</div>
)}
</CommentsContainer>
<CommentBarStyle htmlFor="search-input">
<input
name="commentBar"
type="text"
value={query}
id="search-input"
placeholder="Write a comment..."
onChange={handleOnInputChange}
/>
<AddIcon
className="fas fa-plus"
onClick={() => postComment(query, user)}
/>
</CommentBarStyle>
</Container>
);
}
export default CommentsPage;

Your useEffect will run every time when messages state is change. Change your useEffect like this:
useEffect(() => {
fetchComments();
},[null]);

You are updating the 'messages' state inside the fetchComments... so everytime this state is updated your useEffect will be called because it has the messages as a dependency.
useEffect(() => {
fetchComments();
}, [messages]);
//calls backend to fetch data from the messages collection on mongodb
async function fetchComments() {
const response = await axios.get("http://localhost:5000/messages");
setMessages(response.data);
}
Try this:
useEffect(() => {
fetchComments();
}, []);
If you do it in that way, your component will call the fetchComments in the mount phase instead of calling it everytime that messages state changes.

Related

React component doesn't render unless I refresh & duplicate render

I've been working on React/Redux app with firestore database
In my app I have simple POST request sent when the user send a message in the input field
and the data the user enters supposed to render in the same page without the need to refresh but I do still need to refresh even without deps in my useEffect!
Here's my code :
Post component
{posts.length > 0 &&
[...posts].map(({ id, data: { message, name, job, avatarUrl } }) => (
<Post
key={id}
name={name}
job={job}
message={message}
avatarUrl={avatarUrl}
/>
))}
However I also encounter a weird behavior after I refresh which is the components are rendered twice!Although my database be containing only one unique data for each messageThe react app renders it twice ( The querySnapshot from the database being added to the state arrayposts twice
useEffect
useEffect(() => {
querySnapshot();
});
}, []);
Database query:
const q = query(
collection(db, "posts"),
where("type", "==", "post"),
orderBy("postDate", "desc")
);
Retrieving the data :
const [input, setInput] = useState("");
const [posts, setPosts] = useState([]);
const [nextId, setNextId] = useState("0");
const addPost = (e) => {
e.preventDefault();
const docData = {
name: "mo",
job: "zoo",
message: input,
avatarUrl: "https://",
postDate: firebase.firestore.FieldValue.serverTimestamp(),
type: "post",
};
setDoc(doc(db, "posts", nextId.toString()), docData);
setNextId(parseInt(nextId) + 1);
setInput("");
};
async function querySnapshot() {
const querySnapshot = await getDocs(q);
console.log(querySnapshot.docs[0].data().message);
setNextId(querySnapshot.docs.length)
querySnapshot.docs.forEach((doc) => {
let data = {
id: doc.id,
data: doc.data(),
};
if (data && !posts.includes(data.id)) {
setPosts((current) => [...current, data]);
console.log("psts now", posts);
}
});
}
I tried to use the JavaScript Set by creating
useState(new Set()) but the same problem occurred of duplicate elements
I also tried to change deps of useEffect to render when the posts state array changes still not rendering untill I refresh
The duplication reason would be caused by setPosts(), I have updated the code as below, try to avoid setting the value inside the loop.
async function querySnapshot() {
const querySnapshot = await getDocs(q);
setNextId(querySnapshot.docs.length)
const data = querySnapshot.docs.map((doc)=>{
return {id:doc.id, data: doc.data()}
})
setPosts((current) => [...current, data])
}

Batch hook requests and wait for them all to finish in React

I am having troubles figuring out how to wait for a series of requests to finish sent through an API hook in React.
My fetch hook:
export default function useFetch<T>(endpoint: string, config: RequestInit) {
const [data, setData] = useState<T | undefined>();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
// Set headers, body, auth etc.
const fetchData = async ({ body, onSuccess }: RequestConfig = {}) => {
setIsLoading(true);
try {
const response = await fetch(`${BASE_URL}${endpoint}`, config);
if (response.ok) {
const responseData = await response.json().catch((_) => _);
setError("");
setData(responseData);
// Edit: added count of requests
if (onSuccess) {
onSuccess(responseData);
}
setIsLoading(false);
} else {
setIsLoading(false);
// Handle errors
}
} catch (_) {
setError("Could not contact the server. Please try again later.");
setIsLoading(false);
}
};
const state: ResponseState<T> = { data, isLoading, error };
return [state, fetchData] as const;
}
In the app I have a list of items and the backend only takes single requests at a time, so if I update, say five, items I will send five requests. I want to wait for all the requests to finish, so I can tell the user that the updates are successful (e.g. in the form of a toast).
Before I made my fetch hook, I used promises and then it worked great with Promise.all().
Edit:
Now I have the project container that sends the API requests:
function ProjectContainer() {
const { projectId } = useParams();
const [ state, getProject] = useFetch<IProject>();
const [{ data: hasUpdated, isLoading, error }, attachMaterial] =
useFetch<IProject>();
const [{ data: hasUpdated }, detachMaterial] = useFetch<IProject>();
const [totalNumberOfRequests, setTotalNumberOfRequests] = useState(0); // Edit: added count of requests
const [openToast, setOpenToast] = useState(false);
useEffect(() => {
getProject(`projects/${projectId}`);
}, []);
// Edit: added count of requests:
useEffect(() => {
if (hasUpdated && totalNumberOfRequests === 0) {
setOpenToast(true);
}
}, [totalNumberOfRequests]);
// The backend only has attach and detach, so if I want to make an update to a material for a project, I have to delete (detach) the material and attach a new one
const attachNewMaterialToProject = (materialToAttach: IProjectMaterial) => {
setTotalNumberOfRequests(totalNumberOfRequests + 1); // Edit: added count of requests
attachMaterial(`projects/${projectId}/${materialToAttach.materialId!}`, {
body: materialToAttach,
onSuccess: () => setTotalNumberOfRequests(totalNumberOfRequests - 1) // Edit: added count of requests
});
};
const detachMaterialFromProject = (projectMaterialId: number) =>
detachMaterial(`projects/${projectId}/${projectMaterialId}`);
return (
<>
<ProjectMaterials
attachMaterial={attachNewMaterialToProject}
detachMaterial={detachMaterialFromProject}
/>
<Toast
open={(hasUpdated && !isLoading) || openToast}
onClose={() => setOpenToast(false)}
type={error ? "error" : "success"}
text={error || "Save successful"}
/>
</>
);
}
export default ProjectContainer;
The ProjectMaterials component finds the updated fields and calls attach/detach in the parent:
function ProjectMaterials({
// Other props
attachMaterial,
detachMaterial,
}: ProjectMaterialsProps) {
const { control, handleSubmit, formState } =
useForm<ProjectMaterialFormValues>({
defaultValues: { materialBars: initialMaterialBars },
});
const { fields, append } = useFieldArray({
control,
name: "materialBars",
});
const dirtyFields = formState.dirtyFields.materialBars;
const saveAllMaterials = async (dirtyFields) => {
dirtyFields?.forEach((dirtyProjectMaterial) => {
// Check for dirty fields and then call attachMaterial in the parent for each dirty item in the list
if (dirtyProjectMaterial.id) {
detachMaterial(dirtyProjectMaterial.id);
}
attachMaterial(dirtyProjectMaterial);
});
};
const addMaterial = (): void => {
append(defaultMaterialBar);
};
return (
<>
{fields.map(({ id }, index) => (
<MaterialBar key={id} index={index} control={control} />
))}
<Button onClick={addMaterial}>ADD MATERIAL</Button>
<Button onClick={handleSubmit(saveAllMaterials)}>SAVE ALL</Button>
</>
);
}
export default ProjectMaterials;
In the parent I have tried hasUpdated && !isLoading. This is always true after all the requests have finished and thus the toast will never disappear.
So, using a hook API service, is there a way to wait for several requests of the same type to finish, so I can run some code (here show a toast) when I am sure all requests finished successfully (or show an error if not)?
You could add some state to track the number of active requests.
const [totalNumberOfRequests, setTotalNumberOfRequests] = useState(0);
Use functional state updates to correctly update from the previous state's value. This avoids stale closures over the totalNumberOfRequests state in the attachNewMaterialToProject callback scope.
const attachNewMaterialToProject = (materialToAttach: IProjectMaterial) => {
setTotalNumberOfRequests(count => count + 1);
attachMaterial(`projects/${projectId}/${materialToAttach.materialId!}`, {
body: materialToAttach,
onSuccess: () => setTotalNumberOfRequests(count => count - 1);
});
};

Why the flag is showed every time I reload the page after client has been created?

What I have done by far is when a user creates a client, in the top right of the page, is shown a flag(notification), which says "Client has been successfully created".
To do that was a little complex for me, because saving the client to DB, and listing the client to the web page are in two different components. Also, the flag is another component as well.
To save and list the clients I have used Axios since I'm dealing with the backend a lot.
SaveClient.js
export default function SaveClient({}) {
const save = async () => {
const clientParams = {
userName:
currentClient: clientName,
clientId: clientId,
};
await axios
.post(
process.env.REACT_API_CLIENT, clientParams
)
.then((response) => {
navigate("/clientlist", {state: {showFlagCreate: true}}); //passing the state
})
.catch((error) => {;
console.log(error);
});
};
}
ClientList.js
export default function ClientList() {
const { state } = useLocation();
const showFlagCreate = state?.showFlagCreate;
const [clientlist, setClientList] = useState([])
useEffect(() => {
const clientParams = {
userName:
currentClient: clientName,
clientId: clientId,
};
axios
.get(process.env.REACT_API_CLIENT, clientParams)
.then((response) => {
const {data} = response
setClientList(data)
})
.catch((error) => console.log(error));
}, []);
return (
<div>
...
{showFlagCreate && <FlagCreateClient />}
</div>
);
}
FlagCreateClient
export default function FlagCreateClient() {
const [show, setShow] = useState(true);
return (
<div>
<Transition
show={show}
as={Fragment}
<div>
<p>The client is successfully created.</p>
</div>
<div>
<button onClick={() => {setShow(false)}}>
<span>Close</span>
</button>
</div>
</Transition>
<div/>
);
}
The idea is that in the SaveClient component, when a client is saved, in .then() inside the navigate() function, pass a state in a true condition.
Then in the ClinetList component, I call the state using useLocation(), and I passed in the component {showFlagCreate && <FlagCreateClient />}.
By far this logic works, I create a client, the flag is shown after, but when I reload the page after, the flag is shown. When I make other actions in the webpage which might be clicking the edit button and going back to the ClientList component the flag won't show, even if I reload/refresh the page.
How can I fix this bug?

how do I perform conditial query params in axios?

I am trying to build a conditional dynamic react component where makes an API call based on the user interaction, but if the user types something in the search bar. I want to add search= param the otherwise use /list endpoint without query params. I am using currently Axios , and I would like to know some approach to do the following
const FeedsList = () => {
const [feed, setFeed] = useState([]);
const [currentPageUrl, setCurrentPageUrl] = useState("http://localhost:8001/api/v1/feeds/list/")
const performSearch = () => {
//setLoading(true)
api.get(currentPageUrl).then(res => { // axios call
setLoading(false)
setFeed(res.data.results)
}).catch(function(error){
console.log(error);
});
}
const handleSearch = (e) =>{
console.log(e.target.value)
//performSearch();
}
useEffect(() => {
performSearch()
}, [currentPageUrl]);
if (loading) return "Loading..."
}
export const api = axios.create(
{baseURL : 'http://localhost:8001/api/v1/feeds/list/'}
)
user input
<input type="text" placeholder="Enter keyword" onChange={event => handleSearch(event)}/>
Store user input to state, not URL, and then construct your URL from initial value (list) and user input, if any:
const FeedsList = () => {
const [loading, setLoading] = useState(false);
const [feed, setFeed] = useState([]);
const [searchString, setSearchString] = useState("");
const performSearch = (searchString) => {
setLoading(true);
let url = "http://localhost:8001/api/v1/feeds/list/";
// you might want to escape this value, and sanitize input on the server
if (searchString) url = `${url}?search=${searchString}`;
const cancelTokenSource = axios.CancelToken.source();
return api
.get(url, { cancelToken: cancelTokenSource.token })
.then((res) => {
setLoading(false);
setFeed(res.data.results);
})
.catch(function (error) {
console.log(error);
});
return cancelTokenSource;
};
const handleSearch = (event) => {
setSearchString(event.target.value);
};
useEffect(() => {
let token = performSearch(searchString);
return () => token.cancel();
}, [searchString]);
if (loading) return "Loading...";
};
You might want to debounce or throttle requests, so you will not bombard your server with requests on each keystroke
The Axios api allows for passing a second parameter to the get method which is the config for the request being sent. That config object takes a params property that would be parsed and appended onto the url as a query string. The nice thing is if the params object is empty it will be like not passing anything at all. A fuller example here on their GitHub page.
Passing an empty params object is the same as passing no params object at all in terms of what is requested.
// both lines request url looks like this => https://jsonplaceholder.typicode.com/users
axios.get('https://jsonplaceholder.typicode.com/users')
axios.get('https://jsonplaceholder.typicode.com/users', { params: {} })
To answer your question you could conditionally just create the params based on whether there is a value from the search input, something like the following:
const performSearch = () => {
const config = search === '' ? {
params: {}
} : {
params: { search } // same as { search: search }
}
api.get(currentPageUrl, config).then(res => {
// do something with response
}).catch(function(error){
console.log(error);
});
}
An assumption in the above would be that you stored the search value in state somewhere and add it to your useEffect dependencies list, referencing it in performSearch.

Why does my data not update when calling one api and updates well when I call another?

I am trying to create a real time covid-19 data dashboard using React. The data is coming from the desease.sh api and the global figures change regularly so I am sending a new request to the api end point to get new data using setInterval after a minute. But when that new request happens, the data does not change on my app.
Below is my code to the disease api. Which is not working as expected
function CoolTest() {
const [data, setData] = useState([]);
const getData = async () => {
try {
const data = await axios.get("https://disease.sh/v3/covid-19/all");
setData(data.data);
} catch (e) {
console.log(e)
}
};
useEffect(() => {
getData();
setInterval(()=>{
getData();
setData(data => data)
}, 120000)
}, []);
console.log(data.todayCases)
return (
<Container style={{marginTop:'15px'}}>
<h3>New Cases</h3>
<p className="text-muted"> {data.todayCases}</p>
<hr />
</Container>
)
}
Then using the same code, I tried making a request to the Chuck Norris jokes api, which is working. The Joke changes after every new request.
function CoolTest() {
const [jokes, setJokes] = useState([]);
const getJokesData = async () => {
try {
const data = await axios.get("https://api.chucknorris.io/jokes/random");
setJokes(data.data);
} catch (e) {
console.log(e)
}
};
useEffect(() => {
getJokesData();
setInterval(()=>{
getJokesData();
setJokes(jokes => jokes)
}, 120000)
}, []);
console.log(jokes.value)
return (
<Container style={{marginTop:'15px'}}>
<h3>Chuck Norris Jokes</h3>
<p className="text-muted"> {jokes.value}</p>
<hr />
</Container>
)
}
What am I missing?
Concole screenshot.
enter image description here
Data is not changing because the new data is the same as the previous.
you can change:
const data = await axios.get("https://disease.sh/v3/covid-19/all");
setData(data.data);
to
const data = await axios.get("https://disease.sh/v3/covid-19/all");
setData({ todayCases: Math.random() });
to see the view is changing.
in your second code api name is api.chucknorris.io/jokes/random
and gives you each time a random number that is not equal to the previous.
you can see results is this sandbox
and also change:
setInterval(()=>{
getJokesData();
}, 120000)
to:
setInterval(()=>{
getJokesData();
setJokes(jokes => jokes)
}, 120000)```

Resources