Data is not rendered when changing pages with react router - reactjs

I am using the Link component from React-Router to change pages, on the Play.tsx I have data that I am getting using useEffect from Firestore and that data is rendered on page using map function if the array length is more than 1. It works fine until I change the page and go back to /play where Play.tsx is rendered and there is no data at all, the data from firestore is ok but nothing is rendered on the page.
If I am using a normal a tag instead of Link will work fine.
Play.tsx
const [users, setUsers] = useState<any>([]);
// code...
useEffect(() => {
db.collection("users").orderBy("points", "desc").limit(10).get().then((data: any) => {
data.forEach((usr: any) => {
users.push(usr.data());
console.log(users)
})
}).catch((err) => {
console.log("An error has occured: ", err.message)
})
}, [])
return (
<>
{
users.length >= 1 ?
users.map((d: any) => {
return (
<div className="userWrapper" key={Math.random() * 9999999999999999999}>
<Link to={`/user/${d.username}`}><img className="lbImage" src={d.profileImage} /></Link>
<Link to={`/user/${d.username}`}>
<p className="lbUser">{d.username} {d.pro ? <img src={Pro} className="lbPro" /> : null}
</p>
</Link>
<p className="lbPoints">{d.points} Points, {d.rank} ({d.races} Tests)</p>
</div>
)
})
: <div className="lbSpinner"></div>
}
</>
)
OtherComponent.tsx
<Link to="/play"><li>Home</li></Link>
My route: Play.tsx (data rendered) -> OtherComponent.tsx -> Play.tsx (Data is not rendered)
Note: The problem is not at the data itself because I can console.log it, the problem is at the DOM, is like the map function on users is not calling.
Note2: I have a condition there, if I have more than 1 user on the array it will map, if not it will show a spinner, but none of these is rendered when I come back to Play.tsx, and no error is consoled.

In your useEffect hook you are mutating the state directly by doing users.push(usr.data())
Should be using the setUsers function from the useState hook.
useEffect(() => {
db.collection("users").orderBy("points", "desc").limit(10).get().then((data: any) => {
data.forEach((usr: any) => {
setUsers((prevState)=> ([...prevState, usr.data()]));
console.log(users)
})
}).catch((err) => {
console.log("An error has occured: ", err.message)
})
}, [])
Try changing that and see if the state is working properly

Related

How to execute useEffect only once inside looped component

I have component where I have array of data that is being looped using map and rendered a new component base one that and inside the looped component I have a useEffect that fetches the data from the api but it runs same api twice.
Here is the code
I am looping through array of rule_set_versions which is in this case size of 2
const ExpandedContent = ({ experiment }) => {
return experiment.rule_set_versions &&
experiment.rule_set_versions.map((ruleSetVersion) => <RuleSetVersionCollapse key={ruleSetVersion.id} ruleSetVersion={ruleSetVersion} />)
}
const ExperimentsCollapse = ({ experiment }) => {
return <React.Fragment>
<div className={styles.experiment_collapse_root}>
<Collapse>
<Collapse.Panel className={styles.experiment_item} extra={<ExtraTest experiment={experiment} />}>
<ExpandedContent experiment={experiment} />
</Collapse.Panel>
</Collapse>
</div>
</React.Fragment>
}
Here is my RuleSetVersionCollapse snippet
const ruleSet = useSelector(state => state.ruleSet)
React.useEffect(() => {
if (!ruleSet.id) {
dispatch(getRuleSetAction(ruleSetVersion.rule_set_id))
}
}, [dispatch])
And the useEffect runs twice even though the ruleSetVersion.rule_set_id is same on both the case.
Can anyone suggest any way I can solve this issue.
Thanks

React hook renders [object object]

I am new in React and I am not sure what I do wrong. I am trying to use useEffect and save list in useState. But I am getting [object object] back.
I am building simple weather app and I managed to save 1 result into useState, but now I wanna have 3 constant cities showing weather on page load, not depending on what user enteres. Here is the code
const [query, setQuery] = useState('');
const [weather, setWeather] = useState({});
const [weatherConst, setWeatherConst] = useState([]); <-- not working
useEffect(() => {
fetch(`${api.base}group?id=3413829,6618983,2759794&units=metric&APPID=${api.key}`)
.then(res => res.json())
.then(result => {
setWeatherConst(result)
console.log("new list" + result)})
}, []) <-- not working
function apiCall() {
fetch(`${api.base}weather?q=${query}&unit=metric&APPID=${api.key}`)
.then(res => res.json())
.then(result => {
setWeather(result)
setQuery('')
console.log(result)
})
}
When I console log "new list" i get [object object], but when I run link by itself in browser I get list of 3 cities back
Image of result getting back
Here is a quick snippet illustrating the sequence.
We set the initial state to [] as you have.
On the first render we check state.length and because our initial array is empty it renders <h2>Loading...</h2>.
The useEffect runs and waits 1 second before callingsetState([...loadedState]) which triggers a render (the useEffect cleanup runs here and clears the timer).
We check state.length again and now because our array is no longer empty we render state[0].name, state[1].name, state[2].name. (For a known index or limited number of indexes this is ok, but you'll usually want to use state.map())
<script src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect } = React
const loadedState = [{name: 'foo'},{name: 'bar'},{name: 'baz'}]
function App() {
const [state, setState] = useState([])
useEffect(() => {
const timer = setTimeout(() => {
setState([...loadedState]);
}, 1000);
return () => clearTimeout(timer);
},[])
return (
<div className="container">
{state.length ?
(<div>
<p>{state[0].name}</p>
<p>{state[1].name}</p>
<p>{state[2].name}</p>
</div>) : (<h2>Loading...</h2>)
}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
<div id="root"></div>
Note
The answer to your actual question was given in the comments by jonrsharpe.
"new list" + result is string concatenation, which will implicitly call result.toString(), which gives "[object Object]". Use console.log("new list", result) instead, or if you want to make a more readable string form try e.g. JSON.stringify(result, null, 2).
If you would like another example of explanation:
I would like to explain by simple words. When we use "useEffect" or "setSmth (created by useState hook)" hook we need to understand that this is not a simple function which we just call. This is some kind of async function which calls when it needs.
So, for example :
useEffect(() => {
axios
.get(
"https://api.oceandrivers.com:443/v1.0/getForecastPoints/cnarenal/language/en"
)
.then((res) => res.data.data)
.then((res) => {
setWeather(res);
console.log(weather);
})
.catch((e) => {
console.table(e);
});
}, []);
We can not see results in "console.log(weather)" , because setWeather(res) didn't finish work. We see result only after render of component. So, if we create a list:
let weatherList = weather.map((el) => {
return <li key={el.name}> {el.name} </li>;
});
return (
<div className="App">
<ul>{weatherList}</ul>
</div>
);
we'll see all information that we need.
If you would like to see all code: https://codesandbox.io/s/wonderful-feynman-n0h39?file=/src/App.js:503-677
Sorry for my English. If I said smth wrong tell me please, I'll be very appreciated!

React Component is rendering twice

I have no idea why, the first render shows an empty object and the second shows my data:
function RecipeList(props) {
return (
<div>
{console.log(props.recipes)}
{/*{props.recipes.hits.map(r => (*/}
{/* <Recipe initial="lb" title={r.recipe.label} date={'1 Hour Ago'}/>*/}
</div>
)
}
const RECIPES_URL = 'http://cors-anywhere.herokuapp.com/http://test-es.edamam.com/search?i?app_id=426&q=chicken&to=10'
export default function App() {
const classes = useStyles();
const [data, setData] = useState({});
useEffect(() => {
axios.get(RECIPES_URL)
.then(res => {
setData(res.data);
})
.catch(err => {
console.log(err)
})
}, []);
return (
<div className={classes.root}>
<NavBar/>
<RecipeList recipes={data}/>
<Footer/>
</div>
);
}
I don't know why and I have struggled here for over an hour (React newbie), so I must be missing something.
This is the expected behavior. The reason you see two console logs is because, the first time RecipeList is called with no data (empty object), and the second time when the data becomes available. If you would like to render it only when the data is available you could do something like {Object.keys(data).length > 0 && <RecipeList recipes={data}/>}. By the way this is called conditional rendering.
This is perfectly normal, React will render your component first with no data. Then when your axios.get returns and update data, it will be rendered again with the new data

What's the React best practice for getting data that will be used for page render?

I need to get data that will be used for the page that I'm rendering. I'm currently getting the data in a useEffect hook. I don't think all the data has been loaded before the data is being used in the render. It's giving me an error "property lastName of undefined" when I try to use it in the Chip label.
I'm not sure where or how I should be handling the collection of the data since it's going to be used all throughout the page being rendered. Should I collect the data outside the App function?
const App = (props) => {
const [teams] = useState(["3800", "0200", "0325", "0610", "0750", "0810"]);
const [players, setPlayers] = useState([]);
useEffect(() => {
teams.forEach(teamId => {
axios.defaults.headers.common['Authorization'] = authKey;
axios.get(endPoints.roster + teamId)
.then((response) => {
let teamPlayers = response.data.teamPlayers;
teamPlayers.forEach(newPlayer => {
setPlayers(players => [...players, newPlayer]);
})
})
.catch((error) => {
console.log(error);
})
});
}, []);
let numPlayersNode =
<Chip
variant="outlined"
size="small"
label={players[1].lastName}
/>
return (...
You iterate over a teamPlayers array and add them one at a time, updating state each time, but players is always the same so you don't actually add them to state other than the last newPlayer.
Convert
teamPlayers.forEach(newPlayer => {
setPlayers(players => [...players, newPlayer]);
});
to
setPlayers(prevPlayers => [...prevPlayers, ...teamPlayers]);
Adds all new players to the previous list of players using a functional state update.
You also have an initial state of an empty array ([]), so on the first render you won't have any data to access. You can use a truthy check (or guard pattern) to protect against access ... of undefined... errors.
let numPlayersNode =
players[1] ? <Chip
variant="outlined"
size="small"
label={players[1].lastName}
/> : null
You should always create a null check or loading before rendering stuff. because initially that key does not exists. For example
<Chip
variant="outlined"
size="small"
label={players.length > 0 && players[1].lastName}
/>
this is an example of a null check
For loading create a loading state.
When functional component is rendered first, useEffect is executed only after function is returned.
and then, if the state is changed inside of useEffect1, the component will be rendered again. Here is a example
import React, {useEffect, useState} from 'react'
const A = () => {
const [list, setList] = useState([]);
useEffect(() => {
console.log('useEffect');
setList([{a : 1}, {a : 2}]);
}, []);
return (() => {
console.log('return')
return (
<div>
{list[0]?.a}
</div>
)
})()
}
export default A;
if this component is rendered, what happen on the console?
As you can see, the component is rendered before the state is initialized.
In your case, error is happened because players[1] is undefined at first render.
the simple way to fix error, just add null check or optional chaining like players[1]?.lastName.

Accessing Apollo's loading boolean outside of Mutation component

The Mutation component in react-apollo exposes a handy loading boolean in the render prop function which is ideal for adding loaders to the UI whilst a request is being made. In the example below my Button component calls the createPlan function when clicked which initiates a GraphQL mutation. Whilst this is happening a spinner appears on the button courtesy of the loading prop.
<Mutation mutation={CREATE_PLAN}>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
The issue I have is that other aspects of my UI also need to change based on this loading boolean. I have tried lifting the Mutation component up the React tree so that I can manually pass the loading prop down to any components which rely on it, which works, but the page I am building has multiple mutations that can take place at any given time (such as deleting a plan, adding a single item in a plan, deleting a single item in a plan etc.) and having all of these Mutation components sitting at the page-level component feels very messy.
Is there a way that I can access the loading property outside of this Mutation component? If not, what is the best way to handle this problem? I have read that you can manually update the Apollo local state using the update function on the Mutation component (see example below) but I haven't been able to work out how to access the loading value here (plus it feels like accessing the loading property of a specific mutation without having to manually write it to the cache yourself would be a common request).
<Mutation
mutation={CREATE_PLAN}
update={cache => {
cache.writeData({
data: {
createPlanLoading: `I DON"T HAVE ACCESS TO THE LOADING BOOLEAN HERE`,
},
});
}}
>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I face the same problem in my projects and yes, putting all mutations components at the page-level component is very messy. The best way I found to handle this is by creating React states. For instance:
const [createPlanLoading, setCreatePLanLoading] = React.useState(false);
...
<Mutation mutation={CREATE_PLAN} onCompleted={() => setCreatePLanLoading(false)}>
{(createPlan, { loading }) => (
<Button
onClick={() => {
createPlan({ variables: { input: {} } });
setCreatePLanLoading(true);
}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I like the answer with React States. However, when there are many different children it looks messy with so many variables.
I've made a bit update for it for these cases:
const Parent = () => {
const [loadingChilds, setLoading] = useState({});
// check if at least one child item is loading, then show spinner
const loading = Object.values(loadingChilds).reduce((t, value) => t || value, false);
return (
<div>
{loading ? (
<CircularProgress />
) : null}
<Child1 setLoading={setLoading}/>
<Child2 setLoading={setLoading}/>
</div>
);
};
const Child1 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child1 !== loading ? { ...prev, Child1: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 1 Content
</div>
);
};
const Child2 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME2);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child2 !== loading ? { ...prev, Child2: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 2 Content
</div>
);
};

Resources