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'.
Related
Edit - added minimally reproducible example: https://snack.expo.dev/#hdorra/code
I hope everyone can access the snack. So if you add a task, you can see it show up in the log. Click on the circle, it shows as true (meaning it is clicked). Save and refresh and everything is stored (the task) but the checkbox is not. I stripped the code to make it as bare minimum as possible but it shows the problem.
It has been days of me on this error. I am relatively new to stackoverflow so my apologies if my question isn't clear or I am not asking it in the correct format. I am trying to create a to do app in react native that is using async storage. I created a toggle button that saves the toggle to a state. This button is located in a component:
const [checkBoxState, setCheckBoxState] = React.useState(false);
const toggleComplete = () => {
setCheckBoxState(!checkBoxState)
handleEdit();
console.log(checkBoxState)
}
When the user checks on it - seems to be showing up correctly as marked true and false in the console.
Then, this is passed to an edit handler to update the array, again console shows it is the correct state:
const handleEdit = () => {
props.editHandler(props.todoKey, text, checkBoxState);
console.log(text2, checkBoxState)
};
Then it shows that it saved correctly:
const [todos, setTodos] = React.useState([]);
const handleEdit = (todoKey, text, newStatus) => {
const newTodos = [...todos];
const index = newTodos.findIndex(todos => todos.key === todoKey);
newTodos[index] = Object.assign(newTodos[index], {title: text, status: newStatus});
setTodos(newTodos);
console.log(todos, newStatus)
};
The async function to save to the device and load are as follows:
To save:
const saveTodoToUserDevice = async (todos) => {
try {
const stringifyTodos = JSON.stringify(todos);
await AsyncStorage.setItem('todos', stringifyTodos);
} catch (error) {
console.log(error);
}
};
To load from the device:
const getTodosFromUserDevice = async () => {
try {
const todos = await AsyncStorage.getItem('todos');
if (todos != null) {
setTodos(JSON.parse(todos));
console.log("loaded successfully");
}
} catch (error) {
console.log(error);
}
};
So here is the issue - I get the console log that says it is saved correctly and loaded. BUT, when I refresh, the checkbox state is not saved at all, just the title text (so it is saving but the checkbox would always be false (the initial state set). If I clicked on true, it would show as true and then when I refresh, it goes back to false.
I have spent days and days on this and can't figure it out. Any direction would be helpful Thank you!
I have gone through your code and found some errors you are making in different places. In Task.js you can do without that checkBoxState. For that, pass the status to Task as props while rendering it in FlatList, like so:
<Task
key={item.key}
todoKey={item.key}
title={item.title}
status={item.status}
editHandler={handleEdit}
pressHandler={handleDelete}
/>
Then as below, change the button to toggle the status, so you use what's coming from the props and create a function called toggleStatus and pass it to onPress:
<TouchableOpacity onPress={toggleStatus}>
<View
style={[
styles.circle,
!props.status ? styles.completeCircle : styles.incompleteCircle,
]}
></View>
</TouchableOpacity>
The code for toggleStatus:
const toggleStatus = () => {
props.editHandler(props.todoKey, props.title, !props.status);
};
And handleEdit would be simplified to:
const handleEdit = () => {
props.editHandler(props.todoKey, text2, props.status);
setEdit(false);
console.log(props.status);
};
Lastly, in TasksMain.js so you don't replace what's in the storage with that initial array given to useState, make sure saveTodoToUserDevice runs after getTodosFromUserDevice. For that, add the below state in TasksMain.js and slightly change the two functions as follow:
const [loading, setLoading] = React.useState(true);
const saveTodoToUserDevice = async (todos) => {
if (loading) return;
try {
const stringifyTodos = JSON.stringify(todos);
await AsyncStorage.setItem("todos", stringifyTodos);
} catch (error) {
console.log(error);
}
};
const getTodosFromUserDevice = async () => {
try {
const todos = await AsyncStorage.getItem("todos");
if (todos != null) {
setTodos(JSON.parse(todos));
console.log("loaded successfully");
}
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
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)
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.
Hello I need help with showing all of a certain piece of data. my code is below
const [server, setServer] = useState()
const getServer = useCallback(async () => {
// const id = what do I do here??
const unsubscribe = firebase.db.collection("servers").doc('S7FlCYEvxIVDs7MRymnK').onSnapshot(snapshot => {
setServer(snapshot.data())
})
return unsubscribe
},[props.server])
useEffect(() => {
getServer()
}, [props])
console.log(server)
you can see I want to replace the hard coded doc info with an id variable, so in my project I can show multiple servers instead of a hard coded one I choose.
INTRODUCTION
I have a screen which makes some queries to my server:
Fetch user by username (when the user types something in the search input)
Fetch premium users (when screen mounts + pull to refresh)
Fetch young users (when screen mounts + pull to refresh)
I am thinking about moving this to a hook.
This is my current code:
function MyScreen() {
// Fetch by username
const [isSearching, setIsSearching] = useState([]);
const [searchedUsers, setSearchedUsers] = useState([]);
// Premium
const [isLoading, setIsLoading] = useState(true); <--- NOTE THIS
const [premiumUsers, setPremiumUsers] = useState([]);
// Young users
const [youngUsers, setYoungUsers] = useState([]);
/*
FIRST METHOD
*/
const searchUsersByUsername = async (limit = 20) => {
setIsSearching(true);
try {
const result = await api.users.searchUsersByUsername(username, limit);
setSearchedUsers(result);
} catch(err) {
console.log(err);
}
setIsSearching(false);
}
/*
SECOND METHOD
*/
const getPremiumUsers = async (limit = 10) => {
// Note: No loading here
try {
const result = await api.users.getPremiumUsers(limit);
setPremiumUsers(result);
} catch(err) {
console.log(err);
}
}
/*
THIRD METHOD
*/
const getYoungUsers = async (limit = 10) => {
// Note: No loading here
try {
const result = await api.users.getYoungUsers(limit);
setYoungUsersUsers(result);
} catch(err) {
console.log(err);
}
}
// Effects and rendering...
useEffect(() => {
(async () => {
const promises = [getPremiumUsers(), getYoungUsers()];
await Promise.all(promises).catch((err) => {
console.log(err))
});
setIsLoading(false); // <---- NOTE THIS
})();
}, []);
}
PROBLEM
I cannot use the typical useQuery hook, as I am using Firestore Callable Functions. So, the only way to make requests, is to call my api methods (api.users.searchUser...)
As you can see in the code, I have two types of loading indicators (two states):
isSearching (for the searching by username functionality)
isLoading (for fetching premium and young users in parallel)
How can I implement a reusable hook for this logic?
I mean, I need to abstract all this stuff, in order to be able to:
Search users by username in other screens
Search premium users (without fetching young users in parallel)
Search young users (without fetching premium users in parallel)
And, also, to get the loading status of the three queries.
Note: In my current screen, as I have said before, I am using "setIsLoading" for the young and premium users parallel fetching, but maybe (to be more flexible) in other screens I will need the loading status for each logic independently.
Any help or ideas?
You could use a React.useEffect and React.useCallback by each fetch method.
Also, save the loading status individually.
Check this out:
import { useEffect, useCallback } from 'react';
const defaultParams = {
fetchPremiumUsersOnMount: false,
fetchYoungUsersOnMount: false,
searchOnMount: false,
username: '',
limit: 20,
}
function useUsersApi(params = defaultParams) {
const [isSearching, setIsSearching] = useState(false);
const [searchedUsers, setSearchedUsers] = useState([]);
const [premiumUsers, setPremiumUsers] = useState([]);
const [premiumLoading, setPremiumLoading] = useState(false);
const [youngUsers, setYoungUsers] = useState([]);
const [youngLoading, setYoungLoading] = useState(false);
const fetchPremiumUsers = useCallback(async () => {
try {
setPremiumLoading(true);
const result = api.users.getPremiumUsers(params.limit);
setPremiumUsers(result);
} catch (err) {
console.log(err)
} finally {
setPremiumLoading(false);
}
}, [params.limit]);
const fetchYoungUsers = useCallback(async () => {
/* similar logic to `fetchPremiumUsers` */
}, [params.limit, params.]);
const fetchSearchUsers = useCallback(async (username) => {
/* fetch logic here */
}, [params.limit]);
useEffect(() => {
if(params.fetchPremiumUsersOnMount) {
fetchPremiumUsers();
}
}, [params.fetchPremiumUsersOnMount, params.limit]);
useEffect(() => {
if(params.fetchYoungUsersOnMount) {
fetchYoungUsers();
}
}, [params.fetchYoungUsersOnMount, params.limit]);
useEffect(() => {
if(params.fetchSearchUsers) {
fetchSearchUser(params.username);
}
}, [params.searchOnMount, params.limit, params.username]);
return {
isSearching,
searchedUsers,
isLoading: premiumLoading || youngLoading,
premiumUsers,
premiumUsersLoading: premiumLoading,
refreshPremiumUsers: fetchPremiumUsers,
youngUsers,
youngUsersLoading: youngLoading,
refreshYoungUsers: fetchYoungUsers,
}
}