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

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.

Related

Why do my ReactJS changes disappear on refreshing the page

I'm new to React and I'm trying to render a list of Pokemons.
I'm fetching the pokemon names from a local file and then using those names to trigger HTTP calls to my backend server, to get the pokemon images. Here's my code so far:
function PokemonList(props) {
const [pokemonList, setPokemonList] = useState([]);
const [isFetched, setIsFetched] = useState(false);
const [renderedList, setRenderedList] = useState([]);
useEffect(() => {
fetch(raw)
.then((r) => r.text())
.then((text) => {
setPokemonList(text.split("\n"));
setIsFetched(true);
});
}, []);
// I believe this is to blame, but I don't know why. On refresh, pokemonList becomes empty
useEffect(() => {
setRenderedList(populateRenderedList(pokemonList));
}, []);
return !isFetched ? null : (
<div className="list">
{renderedList}
<PaginationBar listSize={renderedList.length} list={renderedList} />
</div>
);
}
function populateRenderedList(pokemonList) {
let pokemonAPI = "https://pokeapi.co/api/v2/pokemon-form/";
const temp = [];
console.log(pokemonList);
pokemonList.forEach((pokemonName) => {
let renderedPokemon = (
<a className="pokemonLink" href={pokemonAPI + pokemonName.toLowerCase()}>
<PokemonDiv name={pokemonName.toLowerCase()} />
<h3>{pokemonName}</h3>
</a>
);
temp.push(renderedPokemon);
});
return temp;
}
As I have commented on the code, the 'pokemonList' renders fine when I make any changes to the PokemonList function. But the moment I refresh my page, 'pokemonList' becomes empty. Why is that?
I previously was not using 'useState' to populate my 'renderedList' list. So I believe the problem is happening because I'm using 'useState' , but I don't know why that's happening.
I had tried making 'renderedList' not a state, but I had to, for I am thinking about passing it as props to another child component, in order to change it's state.

Why is my component failing to run when I call it?

I am struggling to find why my component is not responding to being called by its parent. I am trying to integrate Cloud Firestore with code that previously ran using Redux. My first goal is to populate my List with data from Firestore.
Here are my (simplified) components in question:
// List.js
import React, { useEffect, useState } from "react";
import db from "../../db";
import { onSnapshot, query, collection, orderBy } from "firebase/firestore";
import TaskItem from "./TaskItem";
const List = () => {
const [taskList, setTaskList] = useState([]); // Currently assumes DB never empty, populates on initial render
const [isInitialRender, setIsInitialRender] = useState(true);
// Firestore
const ref = collection(db, "Tasks");
const q = query(ref, orderBy("listIndex"));
useEffect(() => {
// Execute only on initial render
if (isInitialRender) {
// Populate task list
onSnapshot(q, (querySnapshot) => {
setTaskList(() => querySnapshot.docs)
}, (error) => {
console.log(error)
})
};
setIsInitialRender(() => false);
}, []);
return (
<>
<h2>List</h2>
{taskList.forEach((task) => ( // console-logging `task` here will output correct data
<ul key={task.data().key}>
<TaskItem
id={task.data().key}
// docRef={taskDoc}
/>
</ul>
))
}
</>
);
};
export default List;
// TaskItem.js
import React from "react";
const TaskItem = (props) => {
console.log('This will not print')
const submitHandler = () => console.log('Submitted');
return (
<form onSubmit={submitHandler}>
<input
autoFocus
type="text"
/>
</form>
);
};
export default TaskItem;
I have tried:
Populating the state with the data from each document (rather than assigning it directly), then passing the contents as props. This led to (I believe) an infinite loop, and ideally I would like to pass the actual DocumentReference to the TaskItem anyways. So this was a bust for me.
Returning [...querySnapshot.docs], or even (prev) => prev = [...querySnapshot.docs] in the state setter. No response from TaskItem().
Decomposing the taskList state into a new dummy array, and using that array to populate the props for TaskItem.
I know that the task data is being fetched successfully because I can satisfactorily log taskList's contents from the map function in List's return statement. But it seems like TaskItem() never runs.
Does anyone see my error here?
edit: sorry I assumed you were using map. I'm not sure why your forEach isn't working but map would work, from my example
edit 2: you probably are looking to use map because you want to transform every element in the array: JavaScript: Difference between .forEach() and .map()
you forgot to return something from the map, and maybe need {} instead.
try
{taskList.forEach((task) => {
return (
<ul key={task.data().key}>
<TaskItem
id={task.data().key}
// docRef={taskDoc}
/>
</ul>
)
})

React Hook "useEffect" is called conditionally, in supposedly simple get-display-retrieve inputs component

Please help,
I am going around in circles (new to React).
I either get 'React Hook "useState" is called conditionally' and if I change it to be the beneath code,
I get 'React Hook "useEffect" is called conditionally'.
The idea is to
retrieve the data via useQuery and the db record id pased in via props.
if that data is null, e.g. the query used the id=0 (like an insert not an update of a record), then deviceObject to the empty record, else to the retrieved data.
Set 'deviceObject' into state.
i.e. The order is important, but setRow should only be called once, not multiple times, which leads to react crashing with too many renders.
export default function DeviceModal(props) {
const dataRowId = props.dataRowId;
const classes = useStyles();
const [row, setRow] = useState('')
const device = useQuery(getDevice_query, {variables: {id: dataRowId}});
if (device.loading) return <DataLoader/>;
if (device.error) return <p style={{color: 'white'}}>{("GraphQL Error " + device.error)})</p>;
// Create an empty recordObject to be populated and sent back for insert to db.
const emptyDevice = {
id : 0,
deviceId : 0,
deviceClass :{name : '',},
serialNumber: 0,
}
const deviceObject = device.data.getDevice !== null ? device.data.getDevice : emptyDevice;
useEffect(()=>{
setRow(deviceObject)
},[])
const handleSave = (value) => {
};
const HandleChange = e => {
useEffect(()=>{
setRow({...row, [e.target.name]: e.target.value })
},[])
};
return (
<div>
<Modal ...>
<div className={classes.paper}>
<Grid container direction="row" justify="center" alignItems="center">
<Grid item xs={4}>
<TextField
id="deviceId"
name="deviceId"
defaultValue={row.deviceId}
onChange={HandleChange}
/>
</Grid>
</Grid>
{/* 30 other textfields to capture*/}
....
</div>
</Modal>
</div>
)};
Edit as per Long Nguyen:
// Set the device object record to be either the empty record, or the records retrieved from db if those are populated (not null)
let deviceObject = {};
const Component = () => {
deviceObject = device.data.getDevice !== null ? device.data.getDevice: emptyDevice;
return <RefactorComponent />;
}
// Set the device object (empty or populated with db-retrieved rows,) into state
const RefactorComponent = () =>
{
useEffect(()=>{
setRow(deviceObject)
},[deviceObject])
// return ()
}
Component();
You make a call of hooks after condition. It makes the hook that you call does not appear in the same order between renders, because “React relies on the order in which Hooks are called”.
You can find useful information, also the way to solve your problem in this document.
https://reactjs.org/docs/hooks-rules.html#explanation
UPDATE:
You should not place hooks in a handle, of course.
If you still want to keep the original code, you can extract the block after condition to the other component like this
const Component = () => {
if(conditions) return <Other />
// 🚫 bad, this hook order is not fixed
// it can appear or not
useEffect(() => {
...the effect after condition...
});
return (...);
}
const Component = () => {
if(conditions) return <Other />
return <RefactorComponent />
}
const RefactorComponent = () => {
// ✅ ok, the hooks order is fixed
useEffect(() => {
...the effect after condition...
});
return (...);
}

React native: useState not updating correctly

I'm new to react native and currently struggling with an infinite scroll listview. It's a calendar list that need to change depending on the selected company (given as prop). The thing is: the prop (and also the myCompany state are changed, but in the _loadMoreAsync method both prop.company as well as myCompany do hold their initial value.
import * as React from 'react';
import { FlatList } from 'react-native';
import * as Api from '../api/api';
import InfiniteScrollView from 'react-native-infinite-scroll-view';
function CalenderFlatList(props: { company: any }) {
const [myCompany, setMyCompany] = React.useState(null);
const [data, setData] = React.useState([]);
const [canLoadMore, setCanLoadMore] = React.useState(true);
const [startDate, setStartDate] = React.useState(undefined);
let loading = false;
React.useEffect(() => {
setMyCompany(props.company);
}, [props.company]);
React.useEffect(() => {
console.log('set myCompany to ' + (myCompany ? myCompany.name : 'undefined'));
_loadMoreAsync();
}, [myCompany]);
async function _loadMoreAsync() {
if ( loading )
return;
loading = true;
if ( myCompany == null ) {
console.log('no company selected!');
return;
} else {
console.log('use company: ' + myCompany.name);
}
Api.fetchCalendar(myCompany, startDate).then((result: any) => {
// code is a little more complex here to keep the already fetched entries in the list...
setData(result);
// to above code also calculates the last day +1 for the next call
setStartDate(lastDayPlusOne);
loading = false;
});
}
const renderItem = ({ item }) => {
// code to render the item
}
return (
<FlatList
data={data}
renderScrollComponent={props => <InfiniteScrollView {...props} />}
renderItem={renderItem}
keyExtractor={(item: any) => '' + item.uid }
canLoadMore={canLoadMore}
onLoadMoreAsync={() => _loadMoreAsync() }
/>
);
}
What I don't understand here is why myCompany is not updating at all in _loadMoreAsync while startDate updates correctly and loads exactly the next entries for the calendar.
After the prop company changes, I'd expect the following output:
set myCompany to companyName
use company companyName
But instead i get:
set myCompany to companyName
no company selected!
I tried to reduce the code a bit to strip it down to the most important parts. Any suggestions on this?
Google for useEffect stale closure.
When the function is called from useEffect, it is called from a stale context - this is apparently a javascript feature :) So basically the behavior you are experiencing is expected and you need to find a way to work around it.
One way to go may be to add a (optional) parameter to _loadMoreAsync that you pass from useEffect. If this parameter is undefined (which it will be when called from other places), then use the value from state.
Try
<FlatList
data={data}
renderScrollComponent={props => <InfiniteScrollView {...props} />}
renderItem={renderItem}
keyExtractor={(item: any) => '' + item.uid }
canLoadMore={canLoadMore}
onLoadMoreAsync={() => _loadMoreAsync() }
extraData={myCompany}
/>
If your FlatList depends on a state variable, you need to pass that variable in to the extraData prop to trigger a re-rendering of your list. More info here
After sleeping two nights over the problem I solved it by myself. The cause was an influence of another piece of code that used React.useCallback(). And since "useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed" (https://reactjs.org/docs/hooks-reference.html#usecallback) the code worked with the old (or initial) state of the variables.
After creating the whole page new from scratch I found this is the reason for that behavior.

React - UseEffect not re-rendering with new data?

This is my React Hook:
function Student(props){
const [open, setOpen] = useState(false);
const [tags, setTags] = useState([]);
useEffect(()=>{
let input = document.getElementById(tagBar);
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById(tagButton).click();
}
});
},[tags])
const handleClick = () => {
setOpen(!open);
};
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
tagList.push(input.value);
console.log("tag");
console.log(tags);
console.log("taglist");
console.log(tagList);
setTags(tagList);
}
const tagDisplay = tags.map(t => {
return <p>{t}</p>;
})
return(
<div className="tags">
<div>
{tagDisplay}
</div>
<input type='text' id={tagBar} className="tagBar" placeholder="Add a Tag"/>
<button type="submit" id={tagButton} className="hiddenButton" onClick={addTag}></button>
<div>
);
What I am looking to do is be able to add a tag to these student elements (i have multiple but each are independent of each other) and for the added tag to show up in the tag section of my display. I also need this action to be triggerable by hitting enter on the input field.
For reasons I am not sure of, I have to put the enter binding inside useEffect (probably because the input element has not yet been rendered).
Right now when I hit enter with text in the input field, it properly updates the tags/tagList variable, seen through the console.logs however, even though I set tags to be the re-rendering condition in useEffect (and the fact that it is also 1 of my states), my page is not updating with the added tags
You are correct, the element doesn't exist on first render, which is why useEffect can be handy. As to why its not re-rendering, you are passing in tags as a dependency to check for re-render. The problem is, tags is an array, which means it compares the memory reference not the contents.
var myRay = [];
var anotherRay = myRay;
var isSame = myRay === anotherRay; // TRUE
myRay.push('new value');
var isStillSame = myRay === anotherRay; // TRUE
// setTags(sameTagListWithNewElementPushed)
// React says, no change detected, same memory reference, skip
Since your add tag method is pushing new elements into the same array reference, useEffect thinks its the same array and is not re-triggers. On top of that, React will only re-render when its props change, state changes, or a forced re-render is requested. In your case, you aren't changing state. Try this:
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
// Create a new array reference with the same contents
// plus the new input value added at the end
setTags([...tagList, input.value]);
}
If you don't want to use useEffect I believe you can also use useRef to get access to a node when its created. Or you can put the callback directly on the node itself with onKeyDown or onKeyPress
I can find few mistake in your code. First, you attaching event listeners by yourself which is not preferred in react. From the other side if you really need to add listener to DOM inside useEffect you should also clean after you, without that, another's listeners will be added when component re-rendered.
useEffect( () => {
const handleOnKeyDown = ( e ) => { /* code */ }
const element = document.getElementById("example")
element.addEventListener( "keydown", handleOnKeyDown )
return () => element.removeEventListener( "keydown", handleOnKeyDown ) // cleaning after effect
}, [tags])
Better way of handling events with React is by use Synthetic events and components props.
const handleOnKeyDown = event => {
/* code */
}
return (
<input onKeyDown={ handleOnKeyDown } />
)
Second thing is that each React component should have unique key. Without it, React may have trouble rendering the child list correctly and rendering all of them, which can have a bad performance impact with large lists or list items with many children. Be default this key isn't set when you use map so you should take care about this by yourself.
tags.map( (tag, index) => {
return <p key={index}>{tag}</p>;
})
Third, when you trying to add tag you again querying DOM without using react syntax. Also you updating your current state basing on previous version which can causing problems because setState is asynchronous function and sometimes can not update state immediately.
const addTag = newTag => {
setState( prevState => [ ...prevState, ...newTage ] ) // when you want to update state with previous version you should pass callback which always get correct version of state as parameter
}
I hope this review can help you with understanding React.
function Student(props) {
const [tags, setTags] = useState([]);
const [inputValue, setInputValue] = useState("");
const handleOnKeyDown = (e) => {
if (e.keyCode === 13) {
e.preventDefault();
addTag();
}
};
function addTag() {
setTags((prev) => [...prev, inputValue]);
setInputValue("");
}
return (
<div className="tags">
<div>
{tags.map((tag, index) => (
<p key={index}>{tag}</p>
))}
</div>
<input
type="text"
onKeyDown={handleOnKeyDown}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a Tag"
/>
<button type="submit" onClick={addTag}>
ADD
</button>
</div>
);
}

Resources