React app not re-rendering after infinite scroll implementation - javascript

Hi guys hoping someone can help me with an issue. I built a function that fetches user posts from backend and returns them as a response:
const [posts, setPosts] = useState([]);
const newsFeed = async () => {
await axios
.post(`${process.env.REACT_APP_API}/news-feed`)
.then((res) => {
setPosts(res.data);
})
.catch((err) => {
console.log(err);
});
};
This function gets called by useEffect and also whenever a post is submitted, liked, commented on etc. so that users' posts are always re-rendered every time they are modified or added.
It was working fine until I decided to implement Infinite Scroll to my application. I installed the npm package react-infinite-scroll-component and modified my function to look like this:
const [posts, setPosts] = useState([]);
const [page, setPage] = useState(1);
const newsFeed = async () => {
await axios
.post(
`${process.env.REACT_APP_API}/news-feed/${page}`)
.then((res) => {
let newPosts = posts;
newPosts = newPosts.concat(res.data);
setPosts(newPosts);
setPage(page + 1);
})
.catch((err) => {
console.log(err);
});
};
The infinite scroll is working just fine but now the posts are not being re-rendered every time this function gets called and instead I need to refresh the page to see changes. I tried resetting the state of page back to 1 again on my postSubmit/likeHandler functions but this didn't have any effect. I'm not seeing any errors in the console so am unsure what is going on.

Replace let newPosts = posts; with let newPosts = [...posts]; Passing the same array reference to setState will not cause an update, since the value hasn't changed. By using [...posts], you are creating a new array, causing the component to update.

Related

Why does my react function function iterate twice?

I am currently trying out a project with the PokeAPI. And have used his guide for help. I can't get rid of the problem that the function iterates twice when called in the useEffect.
When I run the following code with the getAllPokemons in the useEffect
const PokeListPage = () => {
const [layoutToggle, setLayoutToggle] = useState(false);
const [allPokemons, setAllPokemons] = useState([]);
const [loadPoke, setLoadPoke] = useState(
"https://pokeapi.co/api/v2/pokemon?limit=20"
);
useEffect(() => {
getAllPokemons();
console.log(allPokemons);
}, []);
const getAllPokemons = async () => {
const res = await fetch(loadPoke);
const data = await res.json();
setLoadPoke(data.next);
function createPokemonObject(result) {
result.forEach(async (pokemon) => {
const res = await fetch(
`https://pokeapi.co/api/v2/pokemon/${pokemon.name}`
);
const data = await res.json();
setAllPokemons((currentList) => [...currentList, data]);
});
}
createPokemonObject(data.results);
console.log(allPokemons);
};
I get doublets of the first 20 objects in allPokemons. See output:
enter image description here
But when I remove the function and uses a button to trigger the function it behaves as expected. Which means that the function populates the allPokemon array with one object per pokemon. See output.
enter image description here
I have tried everything from copying entire files from other repositories and with an accuracy I didn't knew I had followed different tutorials but the problem remains. Does anyone know why?
It's bcz, you are rendering your app into a React. Strict mode component that runs specific functions and methods twice as a way to help you detect unintentional side effects. Since the side-effect is a state update, this triggers a rerender.
Use a useEffect to run the effect once when the component mounts.

How to handle refresh for data filtering in React useEffect?

I have the following code in my React component:
const { id } = useParams();
const { tripData, facilityData } = useContext(AppContext);
const [data, setData] = useState([]);
useEffect(() => {
const idResults = facilityData.filter(facility => facility.id === id);
if (idResults.length > 0) {
setData(idResults[0]);
}
}, [])
Where:
[data, SetData] is the state that is used to handle populating a container
facilityData is data accessed from my app context
id is accessed from the URL
What seems to happen is that the data loads the first time without fault, but it errors out when hosted on the actual site (on localhost, it waits and eventually loads). To try to get a better idea of what was happening, I tried the following code:
const { id } = useParams();
const { tripData, facilityData } = useContext(AppContext);
const [data, setData] = useState([]);
useEffect(() => {
const idResults = facilityData.filter(facility => facility.id === id);
if (idResults.length > 0) {
setData(idResults[0]);
} else if (idResults.length === 0) {
console.log(`id: ${id}`)
console.log(`len: ${idResults}`)
}, [])
On localhost, on refresh, it console logs the actual id but then console logs the empty array before finally loading the data.
What I'm wondering is why this is the observed behavior. The "id" value seems to be constantly available, but the filter doesn't seem to run prior to the site loading. Is there a way to prevent this?
EDIT:
This is how I get the data (from Firebase)
App.js
import { collection, getDocs } from "firebase/firestore";
import { db } from "./firebase";
const [truckData, setTruckData] = useState([]);
const [facilityData, setFacilityData] = useState([]);
const [tripData, setTripData] = useState([]);
useEffect(() => {
const fetchData = async (resource, setter) => {
let list = [];
try {
const querySnapshot = await getDocs(collection(db, resource));
querySnapshot.forEach((doc) => {
let docData = doc.data();
if (resource === "trips") {
docData.startDate = docData.startDate.toDate();
docData.endDate = docData.endDate.toDate();
}
list.push({ id: doc.id, ...docData });
});
setter(list);
} catch (error) {
console.log(error);
}
};
fetchData("trucks", setTruckData);
fetchData("facilities", setFacilityData);
fetchData("trips", setTripData);
}, []);
The app is at logi-dashboard, if that helps any.
EDIT Turns out the issue was with my hosting service, not the project. Go figure.
Based on my understanding, it seems like the facilityData on which you are trying to apply filter and which is coming from AppContext(Context hook variable) is found to be empty array when the useEffect code is getting executed, this might be scene if you are hitting any API to get the data into facility but the API response is not coming till the time useEffect is getting executed or any other source which is not populating the facilityData until useEffect runs.
In that case, you can add facilityData in the dependency array of useEffect, which will help the useEffect execute again once the facilityData is populated(updated)

Reset React state before a render

I'm building a web application that consumes TMDB Api. I have the following code that fetch all information about a TV Show
export const useShowInfoFetch = ({showId}) => {
const [data, setData] = useState({})
const [loading, setLoading] = useState(false)
const [_error, _setError] = useState(false)
const fetchShowInfo = useCallback(() => {
setLoading(true)
try {
axios.get(getShowInfo(showId))
.then(response => {
setData(response.data)
})
} catch (error) {
_setError(true)
} finally {
setLoading(false)
}
}, [showId])
useEffect(() => {
fetchShowInfo()
}, [fetchShowInfo])
return [data, loading, _error]
}
All the information fetched is displayed in page, that also has Links with react-router-dom. Those links goes to another tv show page.
The problem is that when I'm in a page with a tv show that has X amount of seasons and I click a tv show with less seasons, the seasons from the page I was are persisting for a little bit of time. So, when I fetch the information for each season I got a 404 in the page that has less seasons.
Here is a screenshot of the error
The orange circle is what it's displayed since I click the tv show with less seasons.
As you can see, the seasons from the previous page are persisting for a little time, and because The Alienist has only 2 seasons (not 9) I get the 404. You can also note that latter, the correct amount of seasons are displayed.
I've tried to add a cleanup method in the useEffect hook. Something like this:
useEffect(() => {
fetchShowInfo()
return function cleanup() {
setData({})
}
}, [fetchShowInfo])
But this did not work.
I know that I can handle that with a catch after the then Axios promise, but I want to figure out why this is happening and fix the issue with a good solution instead of avoiding it.
Any help is welcomed and I can share the repository with all the code if needed.
EDIT:
To display the similar movies I use another custom hook
export const useSimilarFetch = (elementType, elementId) => {
const [similarElements, setSimilarElements] = useState({elements: []})
const [similarLoading, setSimilarLoading] = useState(false)
const [_error, _setError] = useState(false)
const fetchSimilarElements = useCallback(async (endpoint) => {
console.log(">>> fetching similar elements <<<")
setSimilarLoading(true)
try {
await axios.get(endpoint)
.then(response => {
setSimilarElements(() => ({
elements: [...response.data.results],
currentPage: response.data.page,
totalPages: response.data.total_pages
}))
})
} catch (error) {
_setError(true)
} finally {
setSimilarLoading(false)
}
}, [])
useEffect(() => {
fetchSimilarElements(getSimilar(elementType, elementId));
}, [fetchSimilarElements, elementType, elementId])
return [{similarElements, similarLoading, _error}, fetchSimilarElements]
}
Then, in my ShowInfoComponent I call all the needed hooks like this:
const {showId} = useParams()
const [data, loading, _error] = useShowInfoFetch({showId})
const [{similarElements, similarLoading}] = useSimilarFetch("tv", showId)
Thanks.
By the time showId changes, data has to wait one additional render cycle, so showId is already used even though data has not yet been fetched. The UI relies on both showId and data, yet data depends on showId. One way to solve this could be having your UI to rely on data alone. What about the id? Add it to data for example. We merely want to avoid the desynchronization.
Something like this:
export const useShowInfoFetch = ({showId}) => {
const [data, setData] = useState({})
const [loading, setLoading] = useState(false)
const [_error, _setError] = useState(false)
const fetchShowInfo = useCallback(() => {
setLoading(true)
try {
axios.get(getShowInfo(showId))
.then(response => {
setData({ id: showId, info: response.data})
})
} catch (error) {
_setError(true)
} finally {
setLoading(false)
}
}, [showId])
useEffect(() => {
fetchShowInfo()
}, [fetchShowInfo])
return [data, loading, _error]
}
Then use data.id to build your links.
If response.data already contains the id, then even better, use that.
That's just an example, of course but hopefully you get the idea.
I might be wrong but I believe you are not watching the correct value on the useEffect. You should be watching showId and not the function fetchShowInfo. That is:
useEffect(() => {
fetchShowInfo()
}, [showId]) --> HERE
And as you are memoized the callback, if you are watching the wrong variable then you will get back the 'last answered'.

How to re-render react component depends on a file?

I have a file that stores an array of objects. I have a component that fetches data from this file then render the list. The file could be updated somewhere else, I need the component to be updated if the file is modified. I have following code example
const header = () => {
const [list, setList] = useState([]);
// fetch
useEffect(() => {
const loadList = async () => {
const tempList = await getList("/getlist"); // get call to fetch data from file
setList(tempList);
};
loadList ();
}, [list]);
// function to render content
const renderList = () => {
return list.map(obj => (
<div key={obj.name}>
{obj.name}
</div>
));
};
return (
<div>{renderList()}</div>
)
}
// get call
router.get('/getlist',
asyncWrapper(async (req, res) => {
const result = await getList();
res.status(200).json(result).end();
})
);
const getList= async () => {
const list = JSON.parse(await fs.readFile(listPath));
return list;
}
Code has been simplified. If I remove the list from useEffect, then it will only render once and will never update unless I refresh the page. If I include list there, loadList() will get called constantly, and component will get re-rendered again and again. This is not the behavior I want. I am just wondering without making header component async component, how do I only re-render this component when the file is changed?
Thank you very much.
There are two approaches you can take to this:
Polling
Request the URL on an interval, and clear it when the component is unmounted.
Replace loadList () with:
const interval = setInterval(loadList, 60000); // Adjust interval as desired
return () => clearInterval(interval)
Make sure the cache control headers set in the response to /getlist don't stop the browser from noticing updates.
Server push
Rip out your current code to get the data and replace it with something using websockets, possibly via Socket.IO. (There are plenty of tutorials for using Socket.io with React that can be found with Google, but its rather too involved to be part of a SO answer).

Firebase firestore onSnapshot not picking up changes and rendering on ReactJs app

We have a ReactJs application which used Firebase. When you first register for the application and you log in for the first time, the app loads a document from Firebase using the onSnapshot method, and renders a component that uses the data in the view. The problem is, when we make changes to the loaded document and those changes are written back to Firebase, the ReactJs application isn't picking up that those changes have been applied and so the view isn't updated. If however, we refresh the app, the changes we've made are rendered and future changes are picked up as normal and everything is fine.
Here is the code where we are calling the onSnapshot method. You can see I've stuck some debugging text in there, this is only rendered once when the document is not loaded, so I know that the problem is that this isn't picking up the document changes in firestore.
function useHub(hubId) {
console.debug("useHub");
const Firebase = React.useContext(FirebaseContext);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(false);
const [hub, setHub] = React.useState(null);
React.useEffect(() => {
console.debug("useHub: useEffect");
setHub(null);
console.log('****************************');
console.log("Hub ID " + hubId);
if (hubId) {
const unsubscribe = Firebase.firestore()
.collection("hubs")
.doc(hubId)
.onSnapshot(
(doc) => {
const hubDoc = Object.assign({}, { id: doc.id }, doc.data());
console.log('***************************');
console.log('***************************');
console.log(hubDoc);
console.log('***************************');
console.log('***************************');
setHub(hubDoc);
setLoading(false);
setError(false);
},
(error) => {
console.error("Error:", error);
setError(error.message);
setLoading(false);
}
);
return () => unsubscribe();
}
}, [Firebase, hubId]);
return {
loading,
error,
hub,
};
}
export { useHub };
What are the possible reasons why this would be occuring?
You may consider to have your component update itself as a workaround. I have found this document here, which describes the details.

Categories

Resources