"Partial" re-render after useState updates state and mapping array - javascript

I have a "weird" problem. I have a code in which after downloading data from backend, I update the states, and simply display information about logged user.
The logic downloads user data from server, updates state, and shows current information.
The problem is - only parts of information changes, like user score, but not his his position (index) from array (index is currentPosition in DOM structure)
It looks like this - logic file:
const [usersScoreList, setUsersScoreList] = useState([])
const [liderScore, setLiderScore] = useState('')
const [idle, setIdle] = useState(true)
const fetchUsersScore = async () => {
setIdle(false)
try {
const { data, error } = await getUsersRank({
page: 1,
limit: 0,
sort: usersSort,
})
if (error) throw error
const scoreData = data?.data
const HighestScore = Math.max(...scoreData.map((user) => user.score))
setUsersScoreList((prevData) => [...prevData, ...scoreData])
setLiderScore(HighestScore)
} catch (err) {
console.error(err)
}
}
useEffect(() => {
const abortController = new AbortController()
idle && fetchUsersScore()
return () => abortController.abort()
}, [idle])
Main file -
const { usersScoreList, liderScore } = useScoreLogic()
const [updatedList, setUpdatedList] = useState(usersScoreList)
useEffect(() => setUpdatedList(usersScoreList), [usersScoreList])
const { user } = useAuth()
const { id } = user || {}
const current = updatedList.map((user) => user._id).indexOf(id) + 1
<ScoreBoard
id={id}
score={user.score}
updatedList={updatedList}
currentPosition={current}
liderScore={liderScore}
/>
and component when information is displayed, ScoreBoard:
const ScoreBoard = ({
updatedList,
id,
liderScore,
score,
currentPosition,
}) => {
const { t } = useTranslation()
return (
<ScoreWrapper>
{updatedList?.map(
(user) =>
user._id === id && (
<div>
<StyledTypography>
{t('Rank Position')}: {currentPosition}
</StyledTypography>
<StyledTypography>
{score} {t('points')}
</StyledTypography>
{user.score === liderScore ? (
<StyledTypography>
{t('Congratulations, you are first!')}
</StyledTypography>
) : (
<StyledTypography>
{t('Score behind leader')}: {liderScore - score}
</StyledTypography>
)}
</div>
)
)}
</ScoreWrapper>
)
}
and when the userScoreList in logic is updated (and thus,updatedList in Main file, by useEffect) everything is re-rendered in ScoreBoard (score, score to leader) but not the current position, which is based on index from updatedList array, (const current in main file).
This is a little bit weird. Why the updatedList and usersScoreList arrays changes, user score changes, but not the user index from array while mapping ? (i checked in console.log, user index is based upon score, and yes, during mounting state, the index in arrays are also changed)
If so, why currentPosition is not re-rendered like user score ?
It works only when i refresh the page, THEN the new user index is displayed like other informations.

Can you please refactor your useEffect and write it like that?
useEffect(() =>{
setUpdatedList(usersScoreList)
}, [usersScoreList])
I think the way you do it without curly braces it returns as a cleanup

Related

React State / DOM Not Updating When All Items Deleted

I'm building an app with React and Firebase Realtime Database. Objects are added to an array and sent to the database.
The arrays are updated in React and the result is sent to the database.
The functionality to remove items/objects from the list works fine when there are more than one (i.e. button clicked, database, DOM and state updated immediately).
However, whenever there's one item left and you click its delete button, it's deleted from the database but the state and React DOM aren't updated - you have to refresh the page for it to be removed.
I've tried using different methods to update the database in case it triggered a different response but that didn't work - any ideas would be greatly appreciated:
import React, {useState, useEffect} from 'react'
import { Button } from "react-bootstrap";
import Exercise from "./Exercise";
import AddNewWorkout from "./AddNewWorkout";
import { v4 as uuidv4 } from "uuid";
import WorkoutComponent from './WorkoutComponent';
import AddNewExercise from "./AddNewExercise"
import { database, set, ref, onValue, update } from "../firebase"
const Dashboard = ({user}) => {
const [selectedWorkout, setSelectedWorkout] = useState();
const [workouts, setWorkouts] = useState([])
const [creatingNewWorkout, setCreatingNewWorkout] = useState(false);
const [addingNewExercise, setAddingNewExercise] = useState(false)
function selectWorkout(number) {
const selection = [...workouts].filter(workout => number == workout.id);
setSelectedWorkout(selection[0])
}
function toggleNewWorkoutStatus(e) {
e.preventDefault()
setCreatingNewWorkout(creatingNewWorkout => !creatingNewWorkout)
}
function toggleNewExerciseStatus() {
setAddingNewExercise(addingNewExercise => !addingNewExercise)
}
function writeData() {
const newWorkouts = [...workouts]
const workoutTitle = document.getElementById("workoutTitle").value || new Date(Date.now()).toString()
const workoutDate = document.getElementById("workoutDate").value;
newWorkouts.push({
id: uuidv4(),
title: workoutTitle,
date: workoutDate,
exercises: []
})
set(ref(database, `${user.uid}/workouts/`), newWorkouts )
}
function addWorkoutToListDB(e) {
e.preventDefault();
writeData(user.uid)
}
function removeWorkoutFromList(id) {
const newWorkouts = [...workouts].filter(workout => id !== workout.id);
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
function addExerciseToWorkout(e) {
e.preventDefault();
if (selectedWorkout === undefined) {
alert("No workout selected")
return
}
const newWorkouts = [...workouts]
const exerciseID = uuidv4();
const exerciseName = document.getElementById("exerciseName").value
const exerciseSets = document.getElementById("exerciseSets").value
const exerciseReps = document.getElementById("exerciseReps").value
const exerciseWeight = document.getElementById("exerciseWeight").value
const exercisetTarget = document.getElementById("exercisetTarget").checked
const exerciseNotes = document.getElementById("exerciseNotes").value;
const newExercise = {
id: exerciseID,
name: exerciseName,
sets: exerciseSets,
reps: exerciseReps,
weight: `${exerciseWeight}kg`,
target: exercisetTarget,
notes: exerciseNotes,
}
for (let key of newWorkouts) {
if (key.id === selectedWorkout.id) {
if (key.exercises) {
key.exercises.push(newExercise)
} else {
key.exercises = [newExercise]
}
}
}
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
function removeExerciseFromWorkout(id) {
const newWorkouts = [...workouts];
for (let workout of newWorkouts) {
if(selectedWorkout.id === workout.id) {
if (!workout.exercises) {return}
workout.exercises = workout.exercises.filter(exercise => exercise.id !== id)
}
}
const newSelectedWorkout = {...selectedWorkout}
newSelectedWorkout.exercises = newSelectedWorkout.exercises.filter(exercise => exercise.id !== id)
setSelectedWorkout(newSelectedWorkout)
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
useEffect(() => {
function getWorkoutData() {
const dbRef = ref(database, `${user.uid}`);
onValue(dbRef, snapshot => {
if (snapshot.val()) {
console.log(snapshot.val().workouts)
setWorkouts(workouts => workouts = snapshot.val().workouts)
}
}
)
}
getWorkoutData()
},[])
return (
<div>
{creatingNewWorkout && <AddNewWorkout addWorkoutToListDB={addWorkoutToListDB} toggleNewWorkoutStatus={toggleNewWorkoutStatus} /> }
<div id="workoutDiv">
<h2>Workouts</h2><p>{selectedWorkout ? selectedWorkout.title : "No workout selected"}</p>
<Button type="button" onClick={toggleNewWorkoutStatus} className="btn btn-primary">Add New Workout</Button>
{workouts && workouts.map(workout => <WorkoutComponent key={workout.id} removeWorkoutFromList={removeWorkoutFromList} selectWorkout={selectWorkout} workout={workout}/> )}
</div>
<div>
<h2>Exercise</h2>
{addingNewExercise && <AddNewExercise selectedWorkout={selectedWorkout} addExerciseToWorkout={addExerciseToWorkout} toggleNewExerciseStatus={toggleNewExerciseStatus}/> }
<Button type="button" onClick={toggleNewExerciseStatus} className="btn btn-primary">Add New Exercise</Button>
{selectedWorkout && selectedWorkout.exercises && selectedWorkout.exercises.map(exercise => <Exercise removeExerciseFromWorkout={removeExerciseFromWorkout} key={exercise.id} exercise={exercise}/>)}
</div>
</div>
)
}
export default Dashboard
If it helps, the data flow I'm working to is:
New array copied from state
New array updated as necessary
New array sent to database
Database listener triggers download of new array
New array saved to state
I have tried to use different methods (set, update and remove) in case that triggered the onValue function.
I have also tried to send null values and deleting empty nodes if the array that will be sent to the db is empty.
The above methods didn't have any impact, there was still a problem with the last array element that was only resolved by refreshing the browser.
I have tried to remove the array dependency and add the workout state as a dependency, resulting in the following error: "Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."
I think I understand where the issue was:
In the useEffect call, I set up the state to only be updated if the value in returned from the database was null (to prevent an error I ran into). However, this meant that state wasn't being updated at all when I deleted the last item from the array.
I appear to have fixed this by adding an else clause.
useEffect(() => {
function getWorkoutData() {
const dbRef = ref(database, `${user.uid}`);
onValue(dbRef, snapshot => {
if (snapshot.val()) {
console.log(snapshot.val().workouts)
setWorkouts(workouts => workouts = snapshot.val().workouts)
} else {
setWorkouts(workouts => workouts = [])
}
}
)
}
getWorkoutData()
},[])
`````

Have multiple GET requests and call state setter once they have all completed

I'm trying to render a list of favorite movies based on signed-in user's list.The flow is following:
I have ID of favorited movies in the firebase DTB for each user
User visits the "favorited" page and the collection of favorited movies is updated
Then the API call for movieDB is called for each movie to render movie list
Unfortunately I was able to update the array of objects only via push, which resulted in calling setContent(items) for every item which means that content variable exists even with 1st iteration, leaving the option to render the "trending" div as value is truth not rendering full content.
How can I either refactor the useEffect? Change the render conditions for "Trending" to be sure, that all values in content var are finished updating via API?
const Favorite = () => {
const { user } = useAuthContext();
const { documents: movies } = useCollection("favMovies", ["uid", "==", user.uid]); // only fetch movies for loged user
const [page, setPage] = useState(1);
const [content, setContent] = useState([]);
const [numOfPages, setNumOfPages] = useState();
useEffect(() => {
const items = [];
movies &&
movies.forEach(async (movie) => {
try {
const response = await axios.get(
`https://api.themoviedb.org/3/${movie.mediaType}/${movie.id}?api_key=${process.env.REACT_APP_API_KEY}&language=en-US`
);
items.push(response.data);
setContent(items);
} catch (error) {
console.error(error);
}
});
}, [movies]);
return (
<div>
<span className="pageTitle">Trending</span>
<div className="trending">
{content &&
content.map((con) => (
<SingleContent
key={con.id}
id={con.id}
poster={con.poster_path}
title={con.title || con.name}
date={con.first_air_date || con.release_date}
mediaType={con.media_type}
voteAverage={con.vote_average}
/>
))}
</div>
<CustomPagination setPage={setPage} />
</div>
);
};
export default Favorite;
You can overcome your problem by using Promise.all(). For that change your useEffect code to:
useEffect(() => {
const fectchMovies = async () => {
if (!movies) return;
try {
const promises = movies.map((movie) =>
axios.get(
`https://api.themoviedb.org/3/${movie.mediaType}/${movie.id}?api_key=${process.env.REACT_APP_API_KEY}&language=en-US`
)
);
const content = await Promise.all(promises);
setContent(content.map(c => c.data));
} catch (error) {
console.error(error);
}
};
fectchMovies();
}, [movies]);

ReactNative AsyncStorage, add posts to "favorites", but Screen that shows favorited posts rendering "outdated" list. How to update it on screen visit?

Using React Native Async Storage. I have a single storage item "favorites" which holds an array of post IDs. Works great, adding and removing articles successfully, but there is a problem:
In "Favorites" TabScreen - showing all currently favorited posts - it works but is rendering an outdated list of items.
E.g. Load the app (Expo), the Favorites screen shows current list, but if I go ahead and a remove an item from the array, and go back to the Favorites screen, it still shows the removed item. Same if I add a new item and navigate back to Favorite screen, new item missing.
It only updates if I reload the app.
If you don't mind taking a look, here's the relevant code:
const POSTS_QUERY = gql`
query posts {
posts {
data {
id
attributes {
title
}
}
}
}
`
export default ({ navigation }) => {
const [isLoading, setIsLoading] = useState(true)
const [isAsyncLoading, setIsAsyncLoading] = useState(true)
const [nodes, setNodes] = useState({})
const favCurrent = useRef();
const getFavorites = async () => {
try {
const value = await AsyncStorage.getItem('favorites')
if(value !== null) {
const val = JSON.parse(value)
favCurrent.current = val
setIsAsyncLoading(false)
console.log(favCurrent.current,'favCurrent')
}
} catch(e) {
// error reading value
}
}
getFavorites()
// note the console.log above always shows correct, updated list
const { data, loading, error } = useQuery(POSTS_QUERY)
useEffect(() => {
if (loading) {
return <Loading/>
}
if (error) {
return <Text>Error :( </Text>;
}
const filterNodes = data?.posts?.data.filter(item => favCurrent.current.includes(item.id));
setNodes(filterNodes)
setIsLoading(false)
}, [data]);
if( isLoading || isAsyncLoading ) {
return (
// 'Loading...'
)
} else {
return (
// 'List of items...'
)
}
}
I've also tried a solution from this answer, to no effect:
const [favData, setFavData] = useState(null)
useEffect(() => {
async function getFavorites() {
setFavData(await AsyncStorage.getItem('favorites'));
}
getFavorites()
setIsAsyncLoading(false)
}, []);

How to render a React component that relies on API data?

I am trying to render a component within a component file that relies on data from an outside API. Basically, my return in my component uses a component that is awaiting data, but I get an error of dataRecords is undefined and thus cannot be mapped over.
Hopefully my code will explain this better:
// Component.js
export const History = () => {
const [dateRecords, setDateRecords] = useState(0)
const { data, loading } = useGetRecords() // A custom hook to get the data
useEffect(() => {
fetchData()
}, [loading, data])
const fetchData = async () => {
try {
let records = await data
setDateRecords(records)
} catch (err) {
console.error(err)
}
}
// Below: Render component to be used in the component return
const GameItem = ({ game }) => {
return <div>{game.name}</div>
}
// When I map over dateRecords, I get an error that it is undefined
const renderRecords = async (GameItem) => {
return await dateRecords.map((game, index) => (
<GameItem key={index} game={game} />
))
}
const GameTable = () => {
return <div>{renderRecords(<GameItem />)}</div>
}
return(
// Don't display anything until dateRecords is loaded
{dateRecords? (
// Only display <GameTable/> if the dateRecords is not empty
{dateRecords.length > 0 && <GameTable/>
)
)
}
If dateRecords is meant to be an array, initialize it to an array instead of a number:
const [dateRecords, setDateRecords] = useState([]);
In this case when the API operation is being performed, anything trying to iterate over dateRecords will simply iterate over an empty array, displaying nothing.
You've set the initial state of dateRecords to 0 which is a number and is not iterable. You should set the initial state to an empty array:
const [dateRecords, setDateRecords] = useState([]);

useEffect re-render different values making impossible build setting screen using async storage

i'm quite new to react-native, i'm trying to implementing a setting screen in my recipe search app. basically the user can choose different filter to avoid some kind of food (like vegan or no-milk ecc.), i thought to make an array with a number for each filter and then in the search page passing the array and apply the filter adding piece of strings for each filter. the thing is: useEffect render the array i'm passing with async-storage empty on the first render, it fulfill only on the second render, how can i take the filled array instead of the empty one?
const [richiesta, setRichiesta] = React.useState('');
const [data, setData] = React.useState([]);
const [ricerca, setRicerca] = React.useState("");
const [preferenza, setPreferenza] = React.useState([]);
let searchString = `https://api.edamam.com/search?q=${ricerca}&app_id=${APP_ID}&app_key=${APP_KEY}`;
useEffect(() => {
getMyValue();
getPreferences(preferenza);
},[])
const getMyValue = async () => {
try{
const x = await AsyncStorage.getItem('preferenza')
setPreferenza(JSON.parse(x));
} catch(err){console.log(err)}
}
const getPreferences = (preferenza) => {
if(preferenza === 1){
searchString = searchString.concat('&health=vegan')
}
else { console.log("error")}
}
//useEffect
useEffect(() => {
getRecipes();
}, [ricerca])
//fetching data
const getRecipes = async () => {
const response = await fetch(searchString);
const data = await response.json();
setData(data.hits);
}
//funzione ricerca (solo scrittura)
const onChangeSearch = query => setRichiesta(query);
//funzione modifica stato di ricerca
const getSearch = () => {
setRicerca(richiesta);
}
//barra ricerca e mapping data
return(
<SafeAreaView style={styles.container}>
<Searchbar
placeholder="Cerca"
onChangeText={onChangeSearch}
value={richiesta}
onIconPress={getSearch}
/>
this is the code, it returns "error" because on the first render the array is empty, but on the second render it fills with the value 1. can anyone help me out please?
By listening to the state of the preferenza. You need to exclude the getPreferences(preferenza); out of the useEffect for the first render and put it in it's own useEffect like this:
...
useEffect(() => {
getMyValue();
}, [])
useEffect(() => {
if( !preferenza.length ) return;
getPreferences(preferenza);
}, [preferenza])
i forgot to put the index of the array in
if(preferenza === 1){
searchString = searchString.concat('&health=vegan')
}
else { console.log("error")}
}
thanks for the answer tho, have a nice day!

Categories