Data initialization in useEffect triggers multiple requests - javascript

This is a follow-up question to this one.
Simply, I'm fetching the same date in two different ways. One by one and all together for updates. I have a simple store with context and useReducer.
My current code is like that:
import React, { useEffect } from "react";
import axios from "axios";
import { useGlobalState } from "./state";
const arr = Array.from(Array(100), (x, i) => i + 1);
function App() {
const [{ posts, init }, dispatch] = useGlobalState();
useEffect(() => {
const getInc = () => {
arr.forEach(async id => {
const res = await axios(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
dispatch({
type: "INC",
payload: res.data
});
});
};
const getAll = async () => {
const promises = arr.map(id =>
axios(`https://jsonplaceholder.typicode.com/posts/${id}`)
);
const res = await Promise.all(promises);
dispatch({
type: "ALL",
payload: res.map(el => el.data)
});
};
if (init) {
getInc();
} else {
getAll();
}
setInterval(() => getAll(), 10000);
}, [dispatch, init]);
return (
<>
<div>{posts.length}</div>
</>
);
}
export default App;
In every interval getAll is triggered twice.Here is a working sandbox.
I added a console.log to reducer part so you can see it runs twice. I can also see it in the network tab.

Try Separate Concerns when using useEffect, like you mentioned "One by one and all together for updates".
function App() {
const [{ init, posts }, dispatch] = useGlobalState();
useEffect(() => {
setInterval(() => getAll(dispatch), 10000);
}, [dispatch]);
useEffect(() => {
init ? getInc(dispatch) : getAll(dispatch);
}, [init, dispatch]);
...
}
Note:
useEffect(() => {
init ? getInc(dispatch) : getAll(dispatch);
}, [init, dispatch]);
After init turns to true, getAll gets called twice, once from upcoming interval and one from the useEffect above.
So in summary, on your Network, first 3 post request intervals with ids [0-99] are:
from getInc
from getAll in useEffect with init
from getAll in interval

Related

How to avoid unnecessary API calls with useEffect?

I'm still beginner to ReactJS and I'm having trouble rendering a list.
I don't know why, all the time calls are being made to my API. Since I don't put any dependency on useEffect, that is, I should only render my function once.
I don't understand why this is happening. Can you tell me what I'm doing wrong?
Here's my code I put into codesandbox.io
import React from "react";
import axios from "axios";
import "./styles.css";
const App = () => {
const BASE_URL = "https://pokeapi.co/api/v2";
const [pokemons, setPokemons] = React.useState([]);
const getAllPokemons = async () => {
const { data } = await axios.get(`${BASE_URL}/pokemon`);
data.results.map((pokemon) => getPokeType(pokemon));
};
const getPokeType = async (pokemon) => {
const { data } = await axios.get(pokemon.url);
setPokemons((prev) => [...prev, data]);
};
React.useEffect(() => {
getAllPokemons();
}, []);
console.log(pokemons);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{pokemons.map((pokemon) => (
<p key={pokemon.id} style={{ color: "blue" }}>
{pokemon.name}
</p>
))}
</div>
);
};
export default App;
Thank you very much in advance.
Your issue is that you are calling setPokemons inside getPokeType (which is called for each data in part). Your useEffect is called just once (as expected) and the ${BASE_URL}/pokemon call is executed just once too. But getPokeType is called 20 times and the pokemons state is changed 20 times as well (once for each instance from data.results).
What I would recommend in your case (instead of what you have now) is:
Create a list of all the pokemons and
Set the state just once at the end.
So something like:
...
const getPokeType = async (pokemon) => {
const { data } = await axios.get(pokemon.url);
return data;
};
const getAllPokemons = async () => {
const { data } = await axios.get(`${BASE_URL}/pokemon`);
const pokemons = await Promise.all(
data.results.map((pokemon) => getPokeType(pokemon))
);
setPokemons(pokemons);
};
React.useEffect(() => {
getAllPokemons();
}, []);
...
I was just having the same issue in my project the way I solved is by moving the function definition inside the useEffect
React.useEffect(() => {
const getAllPokemons = async () => {
const { data } = await axios.get(`${BASE_URL}/pokemon`);
data.results.map((pokemon) => getPokeType(pokemon));
};
getAllPokemons();
}, []);
If this solves your problem please accept the answer.

React Hook useEffect has a missing dependency: 'getContacts'

Before posting the below, I have reviewed similar posts on stackoverflow but none resolved my issue.
I'm new to react and fetching data from firestore database. The below code works as required but getting this prompt within react
import React, {useState, useEffect} from 'react'
import {db} from '../firebase'
const ListRecord = () => {
const [details, setDetails] = useState([]);
useEffect(() => {
getContacts()
},[]);
const getContacts = async() => {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({id: doc.id, value: doc.data()})
});
setDetails(arr);
});
console.log(details);
return details
}
return (
<div>
<h2>List Contact Details</h2>
</div>
)
}
export default ListRecord
As per other similar posts I tried moving the getContacts function inside useEffect body which make the prompt disapper but the getContacts function goes in a continuous loop.
I'm not sure what I'm missing here and any help would be appreciated.
There are different potential solutions:
1. Move getContacts() inside the useEffect() hook:
If you call getContacts() only once and only when the component mounts for the first time, this is probably the most logic solution.
useEffect(() => {
const getContacts = async () => {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({
id: doc.id,
value: doc.data()
})
});
setDetails(arr);
});
//console.log(details);
//return details // why are you returning details?
}
getContacts()
}, [setDetails]); // setDetails() is granted to never change therefore the hook will never re-run
or, of course, you can use an IIFE:
useEffect(() => {
(async function() {
// ... same body as getContacts
})()
}, [setDetails])
2. Use a useCallback() hook:
This is something you might want to do if getContacts() is called more than once (for example, when the component mounts and every time some prop changes or when you click on some button)
const getContacts = useCallback(async () => {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({
id: doc.id,
value: doc.data()
})
});
setDetails(arr);
});
//console.log(details);
//return details // why are you returning details?
}, [setDetail]); // setDetails() is granted to never change therefore getContacts() will never be re-created
useEffect(() => {
getContacts()
}, [getContacts]); // as getContacts() never changes, this will run only once
3. Move getContacts() out of the component and make it an independent function:
This can make sense if you want to reuse the same logic into other components:
// getContacts.js file
// OR this code could be in the ListRecord.js file but **outside** the component,
// although, in this case, solutions (1) or (2) would make more sense
import { db } from 'path/to/firebase'
export async function getContacts() {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({
id: doc.id,
value: doc.data()
})
});
return arr; // this time you HAVE TO return arr
});
}
// ListRecord.js file
import React, { useState, useEffect } from 'react';
import { getContacts } from 'path/to/getContacts.js';
const ListRecord = () => {
const [details, setDetails] = useState([]);
useEffect(async () => {
const arr = await getContacts();
if (arr && arr.length > 0) setDetails(arr);
}, [setDetails]);
//...
}
I suggest you have a look at how useEffect and its dependency list works in the official document.
In short, do the following:
useEffect(() => {
getContacts()
}, [getContacts]);
This means when getContacts changes, the useEffect will be re-run.

Multiple useEffect not working as expected

useEffect(() => {
debugger;
}, [filter]);
// eslint-disable-next-line
useEffect(async () => {
if (parseInt(localStorage.getItem("lastFetchTime")) + 8640000 > Date.now()) {
setRecipeList(JSON.parse(localStorage.getItem("recipeList")));
setIsLoading(false);
} else {
await fetch('https://api.spoonacular.com/recipes/random?number=20&apiKey=3c6b5aedfaf34bb899d1751ea2feb1b2')
.then((resp) => resp.json())
.then((data) => {
setRecipeList(data.recipes);
setIsLoading(false);
localStorage.setItem("recipeList", JSON.stringify(data.recipes));
localStorage.setItem("lastFetchTime", Date.now());
})
}
}, []);
I have these 2 useEffect in my program, the first one, with the listener is not being called even if the filter is changed. But it works if I remove the [] from the 2nd useEffect and the 2nd one runs on loop so I cant use it like that. I saw multiple forums, all of which suggests this should work.
import { useState, useEffect } from "react";
import { render } from "react-dom";
const sleep = (ms: number) => new Promise(
resolve => setTimeout(() => resolve('Resolved'), ms));
function App() {
const [filter, setFilter] = useState({ count: 0 });
const [get, set] = useState(0);
useEffect(() => {
console.log('Here');
}, [filter]);
useEffect(() => {
async function myFunction() {
const res = await sleep(5000)
.then(res => console.log(res));
setFilter({ ...filter, count: filter.count + 1 });
}
myFunction();
}, [get]);
return (
<div>
<p>App {get}</p>
<button onClick={() => set((get: number) => get + 1)}>
Click
</button>
</div>
);
}
render(<App />, document.getElementById("root"));
This minor snippet to be working for me as expected.
useEffect cannot be async. If you want to call an async function in useEffect() you need to do it like this:
EDIT: this is the complete useEffect
useEffect(() => {
async function getData() {
if (
parseInt(localStorage.getItem("lastFetchTime")) + 8640000 >
Date.now()
) {
setRecipeList(JSON.parse(localStorage.getItem("recipeList")));
setIsLoading(false);
} else {
const res = await fetch(
"https://api.spoonacular.com/recipes/random?number=20&apiKey=3c6b5aedfaf34bb899d1751ea2feb1b2"
);
const data = await res.json();
setRecipeList(data.recipes);
setIsLoading(false);
localStorage.setItem("recipeList", JSON.stringify(data.recipes));
localStorage.setItem("lastFetchTime", Date.now());
}
}
getData();
}, []);
I tested it and it worked as expected (I console.log() in the other useEffect())
There's nothing wrong with the useEffect. It's a bullet proof. But you make sure the following things:
Is filter updated during the component did mount?
The debugger will show up if you have open developer tool.
Isfilter updated during the component did update?
The debugger won't show up.
To make sure whenfilter is updated, use another effect hook but this time without dependency array.
useEffect(()=>{
console.log(filter) // analyze in the console
})
And if the value is updated during the update then you don't need to use dependency array but check the changes inside the effect hook by using some state for that as filter is coming from the update (props).
import { useState, useEffect, useCallback } from "react";
function App() {
const [isLoading, setIsLoading] = useState(false);
const [filter, setRecipeList] = useState({});
useEffect(() => {
// debugger;
}, [filter]);
// eslint-disable-next-line
const fetchData = useCallback(async () => {
if (
parseInt(localStorage.getItem("lastFetchTime")) + 8640000 >
Date.now()
) {
setRecipeList(JSON.parse(localStorage.getItem("recipeList")));
setIsLoading(false);
} else {
const data = await fetch(
"https://api.spoonacular.com/recipes/random?number=20&apiKey=3c6b5aedfaf34bb899d1751ea2feb1b2"
).then((resp) => resp.json());
setRecipeList(data.recipes);
setIsLoading(false);
localStorage.setItem("recipeList", JSON.stringify(data.recipes));
localStorage.setItem("lastFetchTime", Date.now());
}
}, []);
useEffect(() => {
setIsLoading(true);
fetchData();
}, [fetchData]);
return (
<div>
<span>{isLoading ? "loading" : "loaded!"}</span>
{!isLoading && filter && <div>filter size:{filter.length}</div>}
</div>
);
}
export default App;
I think it will work properly.
Thanks.

React hooks array passing in number when passed into props of component

I am currently working on a chat application and for some reason every time I pass in my array of messages as a prop to another component it passes in a number to the component instead of the message object. I have tried a lot of different methods of passing it in regarding using multiple components etc but it seems to still be passing in the number of elements for some reason. Any help is appreciated... code is below
Component receiving the props
import React, { useEffect } from 'react'
import Message from '../../Message/Message'
function Messages({ messages }) {
useEffect(() => {
console.log(messages)
}, [messages])
return (
<div>
test
</div>
)
}
export default Messages
// Import React dependencies.
import React, { useEffect, useState, } from "react";
// Import React dependencies.
import io from 'socket.io-client'
import axios from 'axios'
import Messages from './Messages/Messages'
import uuid from 'react-uuid'
import { Redirect } from 'react-router-dom'
// Import the Slate components and React plugin.
const ENDPOINT = 'http://localhost:5000/'
export const socket = io.connect(ENDPOINT)
const LiveChatFunction = ({ group_id }) => {
// Add the initial value when setting up our state.
const [message, setValue] = useState("")
const [user, setUser] = useState("")
const [groupId, setGroup] = useState('')
const [messages, setMessages] = useState([])
const [toLogin, userAuth] = useState(false)
useEffect(() => {
setGroup(group_id)
axios.post('http://localhost:5000/api/users/refresh_token', null, { withCredentials: true }).then(data => {
if (!data.data.accessToken) {
userAuth(true)
}
})
axios.get('http://localhost:5000/api/users/userInfo', { withCredentials: true }).then(data => {
setUser(data.data.user)
})
socket.on(`message-${group_id}`, data => {
setMessages(messages.push(data))
});
axios.get(`http://localhost:5000/live/${group_id}`).then(x => {
console.log(x.data)
})
}, [group_id, messages])
function setClick() {
const data = {
messageId: uuid(),
user,
groupId,
message
}
socket.emit('message', data)
}
if (toLogin) {
return (
<Redirect to="/login" />
)
}
return (
<div>
<input placeholder="message" type="text" onChange={value => {
setValue(value.target.value)
socket.emit('typing-message', { username: user, time: new Date() })
}} />
<button onClick={setClick}>Submit</button>
<Messages messages={messages} />
</div>
)
}
export default LiveChatFunction;
I have added some comments of what I think you can change:
useEffect(() => {
const recieveFunction = (data) => {
//using callback so no dependency on messages
setMessages((messages) => messages.push(data));
};
async function init() {
//next line is pointless, this runs when group_id
// has changed so something must have set it
// setGroup(group_id);
await axios //not sure if this should be done before listening to socket
.post(
'http://localhost:5000/api/users/refresh_token',
null,
{ withCredentials: true }
)
.then((data) => {
if (!data.data.accessToken) {
userAuth(true);
}
});
await axios
.get('http://localhost:5000/api/users/userInfo', {
withCredentials: true,
})
.then((data) => {
setUser(data.data.user);
});
//start listening to socket after user info is set
socket.on(`message-${group_id}`, recieveFunction);
axios
.get(`http://localhost:5000/live/${group_id}`)
.then((x) => {
console.log(x.data);
});
}
init();
//returning cleanup function, guessing socket.off exists
return () =>
socket.off(`message-${group_id}`, recieveFunction);
}, [group_id]); //no messages dependencies
console.log('messages are now:',messages);
If messages is still not set correctly then can you log it
So I think I found your problem:
In your useEffect hook, you're setting messages to the wrong thing.
socket.on(`message-${group_id}`, data => {
setMessages(messages.push(data))
});
An example:
const m = [].push();
console.log(m);
// m === 0
const n = [].push({});
console.log(n);
// n === 1
As you can see this is the index.
So what you need is:
socket.on(`message-${group_id}`, data => {
messages.push(data);
setMessages(messages);
});
This will set messages to the array of messages.

Unable to read state updated by useReducer hook in context provider

I am using useReducer hook to manage my state, but it seems like I have a problem with reading updated state in my context provider.
My context provider is responsible to fetch some remote data and update the state based on responses:
import React, { useEffect } from 'react';
import useAppState from './useAppState';
export const AppContext = React.createContext();
const AppContextProvider = props => {
const [state, dispatch] = useAppState();
const initialFunction = () => {
fetch('/some_path')
.then(res => {
dispatch({ type: 'UPDATE_STATE', res });
});
};
const otherFunction = () => {
fetch('/other_path')
.then(res => {
// why is `state.stateUpdated` here still 'false'????
dispatch({ type: 'DO_SOMETHING_ELSE', res });
});
}
};
const actions = { initialFunction, otherFunction };
useEffect(() => {
initialFunction();
setInterval(otherFunction, 30000);
}, []);
return (
<AppContext.Provider value={{ state, actions }}>
{props.children}
</AppContext.Provider>
)
};
export default AppContextProvider;
and useAppState.js is very simple as:
import { useReducer } from 'react';
const useAppState = () => {
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_STATE':
return {
...state,
stateUpdated: true,
};
case 'DO_SOMETHING_ELSE':
return {
...state,
// whatever else
};
default:
throw new Error();
}
};
const initialState = { stateUpdated: false };
return useReducer(reducer, initialState);
};
export default useAppState;
The question is, as stated in the comment above, why is state.stateUpdated in context provider's otherFunction still false and how could I access state with latest changes in the same function?
state will never change in that function
The reason state will never change in that function is that state is only updated on re-render. Therefore, if you want to access state you have two options:
useRef to see a future value of state (you'll have to modify your reducer to make this work)
const updatedState = useRef(initialState);
const reducer = (state, action) => {
let result;
// Do your switch but don't return, just modify result
updatedState.current = result;
return result;
};
return [...useReducer(reducer, initialState), updatedState];
You could reset your setInterval after every state change so that it would see the most up-to-date state. However, this means that your interval could get interrupted a lot.
const otherFunction = useCallback(() => {
fetch('/other_path')
.then(res => {
// why is `state.stateUpdated` here still 'false'????
dispatch({ type: 'DO_SOMETHING_ELSE', res });
});
}
}, [state.stateUpdated]);
useEffect(() => {
const id = setInterval(otherFunction, 30000);
return () => clearInterval(id);
}, [otherFunction]);

Categories

Resources