React - Array in state is defined, but Array[index] is undefined - javascript

I am writing a react app to fetch vocabulary from a backend database and then display it to the user, along with some information about each particular word. My problem is that I can't get the word to display inside render() as the index of the array is undefined, however according to chrome devtools the array is fully defined.
Here's my code:
import React from 'react';
import "./Vocab.css"
class Vocab extends React.Component {
//HEADER HEADER HEADER
state = {
vocabulary: null,
var2: null
};
componentDidMount(){
//Initializing Routine Here
this.getVocab()
}
getVocab = () => {
var newVocab = [];
if(this.state.vocabulary == null){
newVocab = newVocab;
}
else{
newVocab = [this.state.vocabulary];
}
this.props.HandleFetch("GET", this.props.backLink + "vocab")
.then(r => JSON.parse(r))
.then(data => newVocab.push(data[0]))
.then(this.setState({vocabulary:newVocab}))
}
//In render() variable display and the if/else statements are used exclusively for testing purposes
render() {
var display = "X";
if(this.state.vocabulary == null){
display = "Y";
}
else{
console.log(this.state.vocabulary); //Outputs the array
console.log(this.state.vocabulary[0]) //Outputs "undefined"
display = "Z";
}
return (
<>
<h1>Test</h1>
{display}
</>
);
}
}
export default Vocab;
The output of console.log(this.state.vocabulary):
0:
Definition: "TestDef"
IPA: "TestIPA"
IsCard: true
Language: "TestLang"
LastStudyDate: "2021-01-27"
Mnem: ""
PoS: "TestPO"
Source: "TestDict"
SourceLearned: "TestSource"
Word: "Test"
The output of this.state.vocabulary[0]:
undefined
Additionally, according to chrome devtools and through typing $r.state.vocabulary[0] in the console I get the dictionary at the array's index, which I need:
{Word: "Test", Mnem: "", LastStudyDate: "2021-01-27", SourceLearned: "TestSource", IsCard: true, …}
In the console the array is fully navigable, but the web page itself won't render it. Any idea what I might be doing wrong?
UPDATE
I was able to add a button the onClick goes to a function which prints out vocabulary and vocabulary[0]. This DOES work, but the render() function and componentDidUpdate() function still cannot access vocabulary[0]. I don't see anything in the react documentation that talks about a setState() finishing or any function happening after componentDidUpdate().
Additionally during some earlier testing I tried to console.log newVocab[0] before assigning it to state in a .then string. This worked, but outputted in the console only after all my other console calls, which indicates that it is happening after the component rerender and update cycle, even though I was calling it before I called setState().
My current test code so far:
import "./Vocab.css"
class Vocab extends React.Component {
//HEADER HEADER HEADER
state = {
vocabulary: null,
var2: null
};
componentDidMount(){
//Initializing Routine Here
this.getVocab()
}
componentDidUpdate(){
console.log("Vocab update");
console.log(this.state.vocabulary);
console.log("Vocab[0] update");
console.log(this.state.vocabulary[0]);
///Outputs the vocabulary array, and then 'undefined'
}
getVocab = () => {
var newVocab = [];
if(this.state.vocabulary == null){
newVocab = newVocab;
}
else{
newVocab = this.state.vocabulary;
}
this.props.HandleFetch("GET", this.props.backLink + "vocab")
.then(r => JSON.parse(r))
.then(data => newVocab.push(data[0]))
.then(this.setState({vocabulary:newVocab}))
}
logVocab = () => {
console.log("Vocab");
console.log(this.state.vocabulary);
console.log("Vocab[0]");
console.log(this.state.vocabulary[0]);
///Outputs the vocabulary array, and then the dictionary at index[0].
}
render() {
var display = "X";
if(this.state.vocabulary == null){
display = "Y";
console.log("Y");
}
else{
console.log("Vocab Z");
console.log(this.state.vocabulary);
console.log("Vocab[0] Z");
console.log(this.state.vocabulary[0]);
display = "Z";
///Outputs the vocabulary array, and then 'undefined'
}
return (
<>
<h1>Test</h1>
{this.state.vocabulary ? this.state.vocabulary[0] : ""}
<button onClick = {this.logVocab}> CLICK </button>
</>
);
}
}
export default Vocab;

The problem is the state.vocabulary is still null when you're trying to log out vocabulary[0]. Since this.props.HandleFetch is an async operation, you need to wait until this.state.vocabulary is set, like this:
render() {
const { vocabulary } = this.state;
if (!vocabulary) {
// render nothing if vocabulary is null
return null;
}
// This will log out the first element of vocabulary instead of undefined
console.log(this.state.vocabulary[0]);
return (
...
);
}
UPD: You also have incorrect syntax in getVocab method:
.then(this.setState({vocabulary:newVocab}))
What it does is immediately assign vocabulary to the newVocab variable which is an empty array at the moment of execution. Then, when this.props.HandleFetch gets fulfilled, newVocab is filled with response data.
You can fix this like the following:
this.props.HandleFetch("GET", this.props.backLink + "vocab")
.then(r => JSON.parse(r))
.then(data => this.setState({ vocabulary: data[0] }))

Update: I was able to track down the problem.
The issue was with the .then string. If you use .then but do not pass an argument to the code you want to execute, it looks like the .then portion will just be omitted and it will run without waiting, which is what was causing things to run out of order.
I believe the relevant line from the documentation (here) is
If one or both arguments are omitted or are provided non-functions, then then will be missing the handler(s), but will not generate any errors. If the Promise that then is called on adopts a state (fulfillment or rejection) for which then has no handler, the returned promise adopts the final state of the original Promise on which then was called.
My updated, and working, getVocab() function:
var newVocab = [];
if(this.state.vocabulary == null){
newVocab = newVocab;
}
else{
newVocab = this.state.vocabulary;
}
this.props.HandleFetch("GET", this.props.backLink + "vocab")
.then(r => JSON.parse(r))
.then(data => {newVocab.push(data[0]);
this.setState({vocabulary:newVocab})})
//Outputs correctly due to 'data' argument
}
Thank you to everyone who commented or helped me with this.

Related

React - Make search and filter work on a given data at same time irrespective of the order of change

I am retrieving a list of data and search for a title name from a list of todos, then i have built with Switch Material UI a component to toggle the todos that are completed and not.
You can see the full demo here => https://codesandbox.io/s/so-68247206-forked-sx9kv?file=/src/App.js
Now i want to search the todos title in the list of the todos/tasks that are completed or not completed, depending on where you switch, at the moment the search goes through in all the todos.
To reproduce the case
Launch the app in codesandbox
Switch the toggle
Search for elements (It search in all the todos list and not through the completed or not completed result list )
The relevant code is the following
The search method
const handleSearch = (event) => {
let value = event.target.value.toLowerCase();
let result = [];
result = tasks.filter((data) => {
return data.title.search(value) !== -1;
});
setFilteredData(result);
};
The complete method
const getCompleted = (e, value) => {
let result = [];
let switchValue = e.target.checked;
result = tasks.filter(({ completed }) => completed === switchValue);
setFilteredData(result);
setIsCompleted(!switchValue);
setText(isCompleted ? "Yes" : "No");
};
How can i change my search method to look through the todos completed and not completed?
There's a slight modification to your handleSearch method. When you're filtering your tasks, you can check if you have toggled on the completed switch using the isCompleted state. All you need is an additional condition inside your filter method that takes care of the is completed scenario. The following updated handleSearch method works fine.
const handleSearch = (event) => {
let value = event.target.value.toLowerCase();
let result = [];
console.log(value);
let filterCompleted = false;
if (!isCompleted) filterCompleted = true;
console.log({ filterCompleted });
result = tasks.filter((data) => {
if (filterCompleted)
return data.completed && data.title.search(value) !== -1;
else return !data.completed && data.title.search(value) !== -1;
});
setFilteredData(result);
};
I use a local variable inside handleSearch to check if you have toggled on the completed button. If it's toggled on, while filtering I only filter those tasks that have the completed property set to true and vice versa. You can check that this works here https://codesandbox.io/s/so-68247206-forked-4iz7p?file=/src/App.js.
Note: I feel the isCompleted state is working on a reverse logic. When I toggle on the switch, the isCompleted is false but it should be true instead. You can try fixing this or renaming the state (if the reverse logic was intentional and you only messed up with naming the state). Anyhow this doesn't affect the solution but makes it difficult to debug and adds some confusion.
Your filter in handleSearch searches from the original tasks array which might not be what you want :
const handleSearch = (event) => {
let value = event.target.value.toLowerCase();
let result = tasks; //Assigning initial value to result
if(isCompleted){
result = result.filter(({ completed }) => completed === isCompleted)
} //only if isCompleted is true, filter the array first
result = result.filter((data) => {
return data.title.search(value) !== -1;
}); //Filter the array based on search term
setFilteredData(result);
};
Also, your getCompleted is setting the inverse value of the checkbox with (!switchValue). You can change that along with the extra param value which is not accounted for in your onChange call.
const getCompleted = (e) => {
let switchValue = e.target.checked;
let result = tasks.filter(({ completed }) => completed === switchValue);
setFilteredData(result);
setIsCompleted(switchValue);
setText(switchValue ? "Yes" : "No"); //Is text state variable really required?
};
Here I have tried to fix it : CodeSandbox
That being said, I would recommend not having multiple state variables which can be derived from each other. Your text state variable is only used for view purposes and its value completely relies on isCompleted. You could have used a conditional operator in your return function to show "Yes/No" instead of a new state variable.
You have to save both the search value and switch value in state and use a useEffect to filter the values. Check the below fork of your code
https://codesandbox.io/s/so-68247206-forked-nkuf3?file=/src/App.js

RxJS indexOfQuizId undefined in console

I've started refactoring my code to work with Angular Observables and I'm using the Observable 'of' for the quizzes$ array, but I'm stuck at getting indexOfQuizId working, specifically with findIndex. I am getting an undefined value for this.indexOfQuizId in the console, not sure what I'm doing wrong... Please could you see my code below (in getIndexOfQuizId()) and help to get it to work. Thank you!
In quiz.ts:
export const getQuizzes$: Observable<Quiz[]> = of([quiz data inside array]);
In quiz.service.ts:
export class QuizService {
quizzes$: Observable<Quiz[]>;
...
constructor(...) {
this.quizzes$ = getQuizzes$;
...
}
getQuizzes(): Observable<Quiz[]> {
return this.quizzes$;
}
getIndexOfQuizId() {
const index = this.getQuizzes().pipe(map(quizzes => {
this.indexOfQuizId = quizzes.findIndex((elem) => elem.quizId === this.quizId);
})).subscribe(x => {
console.log(x);
console.log('IOQID: ', this.indexOfQuizId);
});
}
In quiz.component.html, I'm using this.indexOfQuizId to access the quizzes$ array object like this:
this.totalQuestions = this.quizzes$[this.quizService.indexOfQuizId].questions.length;
You have an undefined for this line console.log('IOQID: ', this.indexOfQuizId); and it's quite right.
As you may know, the subscribe is asynchronous, that means that the next line (console.log('IOQID: ', this.indexOfQuizId);) is called before you set any value into this.indexOfQuizId since the map will be called eventually.
Try to move the console.log into the subscribe like this:
getIndexOfQuizId() {
const index = this.getQuizzes().pipe(map(quizzes => {
this.indexOfQuizId = quizzes.findIndex(function(elem, i, obs) { return elem => elem.quizId === this.quizId; });
})).subscribe(x => {
console.log(x);
console.log('IOQID: ', this.indexOfQuizId);
});
}

Cannot empty array in ReastJS useEffect hook

I'm having this table view in React where I fetch an array from API and display it. When the user types something on the table search box, I'm trying to clear the current state array and render the completely new result. But for some reason, the result keeps getting appended to the current set of results.
Here's my code:
const Layout = () => {
var infiniteScrollTimeout = true;
const [apiList, setapiList] = useState([]);
//invoked from child.
const search = (searchParameter) => {
//Clearing the apiList to load new one but the console log after setApiList still logs the old list
setapiList([]); // tried setApiList([...[]]), copying the apiList to another var and emptying it and then setting it too.
console.log(apiList); //logs the old one.
loadApiResults(searchParameter);
};
let url =
AppConfig.api_url + (searchParameter || "");
const loadApiResults = async (searchParameter) => {
let response = await fetch(url + formurlencoded(requestObject), {
method: "get",
headers: headers,
});
let ApiResult = await response.json(); // read response body and parse as JSON
if (ApiResult.status == true) {
//Set the url for next fetch when user scrolls to bottom.
url = ApiResult.next_page_url + (searchParameter || "");
let data;
data = ApiResult.data;
setapiList([...data]);
}
}
useEffect(() => {
loadApiResults();
document.getElementById("myscroll").addEventListener("scroll", () => {
if (
document.getElementById("myscroll").scrollTop +
document.getElementById("myscroll").clientHeight >=
document.getElementById("myscroll").scrollHeight - 10
) {
if (infiniteScrollTimeout == true) {
console.log("END OF PAGE");
loadApiResults();
infiniteScrollTimeout = false;
setTimeout(() => {
infiniteScrollTimeout = true;
}, 1000);
}
}
});
}, []);
return (
<ContentContainer>
<Table
...
/>
</ContentContainer>
);
};
export default Layout;
What am I doing wrong?
UPDATE: I do see a brief moment of the state being reset, on calling the loadApiResult again after resetting the state. The old state comes back. If I remove the call to loadApiResult, the table render stays empty.
add apiList in array as the second parameter in useEffect
You need to use the dependencies feature in your useEffect function
const [searchParameter, setSearchParameter] = useState("");
... mode code ...
useEffect(() => {
loadApiResults();
... more code ...
}, [searchParameter]);
useEffect will automatically trigger whenever the value of searchParameter changes, assuming your input uses setSearchParameter on change

Render a pair of randomly chosen objects (with certain property unmatched) as play cards in ReactJS

I'm making a little game where the user guesses which of the two foods has the most calories. I scraped data on 400+ food items and imported in my component.
I want to make sure that both randomly selected food items do not have the same number of calories. I put a while loop in componentDidMount to check if it's true. If true, it will setState to another randomly selected food – repeat until they're no longer the same number.
For now, I have my while loop's condition set to '!==' for testing. This will be changed to '===' after I know it works the other way. The problem I'm having is that in my console, the foods will always be the same objects because setState isn't working for some reason.
import Foods from "../foods.json";
class Game extends Component {
state = {
firstFood: Foods[Math.floor(Math.random() * Foods.length)],
secondFood: Foods[Math.floor(Math.random() * Foods.length)],
mostCalories: "placeholder"
};
componentDidMount() {
while (
// to check that the two foods' calories are not the same
this.state.firstFood.attributes.calories !==
this.state.secondFood.attributes.calories
) {
console.log(this.state.firstFood.attributes.calories);
console.log(this.state.secondFood.attributes.calories);
debugger;
// an infinite loop because the below setState function doesn't run properly
this.setState({
firstFood: Foods[Math.floor(Math.random() * Foods.length)]
});
}
}
Another issue I'm facing is that in my debugger, when I try to manually enter Foods[Math.floor(Math.random() * Foods.length)], I get a "Food is not defined" although it works in the state initialization. However, when I enter _foods_json__WEBPACK_IMPORTED_MODULE_1__ in console from autofill, I have all of my data there so this works (replaced the word "Foods"):
_foods_json__WEBPACK_IMPORTED_MODULE_1__[Math.floor(Math.random() * _foods_json__WEBPACK_IMPORTED_MODULE_1__.length)]
I can't use that line of code in React, though.
http://www.giphy.com/gifs/UqwKkdF2ok2hlTwlLB
I would suggest slightly different approach for generating a pair of pseudo-random items, that may not save you CPU cycles, but will certainly avoid loops that led you to your current position.
So, the idea is rather simple:
you pick first random item
then you filter out the items with the same nutritional value
you pick another random item from remaining ones
pickTwo = arr => {
const firstItem = arr[0|Math.random()*arr.length],
filteredArr = arr.filter(({value}) => firstItem.value != value),
secondItem = filteredArr[0|Math.random()*filteredArr.length]
return [firstItem, secondItem]
}
You may find the demo of that concept below:
//dependencies
const { useState, useEffect } = React,
{ render } = ReactDOM
//sample data set
const data = [{name:'BBQ Ranch Burger',value:350},{name:'Big Mac',value:540},{name:'Double Cheesburger',value:440},{name:'Bacon Cheddar McChicken',value:540},{name:'Chicken McNuggets (10 piece)',value:440},{name:'Bacon Biscuit',value:350}]
//food card component
const FoodCard = ({name,value}) => (
<div style={{width:100,height:150,display:'table-cell', border:'1px solid black',verticalAlign:'middle',textAlign:'center'}}>{name}<br/>(calories: {value})</div>
)
//game board component
const GameBoard = () => {
//store foods within local state
const [foods,setFoods] = useState([]),
//describe thw way of picking two pseudo-random items
pickTwo = arr => {
const firstItem = arr[0|Math.random()*arr.length],
filteredArr = arr.filter(({value}) => firstItem.value != value),
secondItem = filteredArr[0|Math.random()*filteredArr.length]
return [firstItem, secondItem]
}
//pick two cards on initial render
useEffect(() => setFoods(pickTwo(data)),[])
//return component
return (
<div>
<FoodCard {...foods[0]} />
<FoodCard {...foods[1]} />
<button onClick={() => setFoods(pickTwo(data))}>reset</button>
</div>
)
}
render(
<GameBoard />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
The reason your loop will run endlessly is that, when you call setState the state is not actually immediately updated, there is a lag on it, which has to do with the React component lifecycle.
So instead of calling the setState in the loop. Declare a local variable, update the variable in the loop and when the loop is done, then set the state.
componentDidMount() {
let myFood = this.state.firstFood
while (
// to check that the two foods' calories are not the same
myFood.attributes.calories !==
this.state.secondFood.attributes.calories
) {
myFood = Foods[Math.floor(Math.random() * Foods.length)]
}
this.setState({
firstFood: myFood
});
}
I did not test this in detail but that should give you the idea.
State is not just some variable that you set whenever you want. It is reactive. It causes re-renders. You should pick two foods you want to have, make sure they don't have the same calories and only then do you setState them
There is no reason to setState on componentDidMount, your logic belongs in constructor
const _pickFoods = (allFoods) => {
const firstFood = allFoods[Math.floor(Math.random() * allFoods.length)]
let secondFood
do {
secondFood = allFoods[Math.floor(Math.random() * allFoods.length)]
} while (firstFood.attributes.calories !== secondFood.attributes.calories)
return { firstFood, secondFood }
}
class Game extends Component {
constructor(props) {
super(props)
const { firstFood, secondFood } = _pickFoods(Foods)
this.state = {
firstFood,
secondFood
}
}
...
}
That is if you want this while logic in your foods. It will loop forever if your Foods array consists of unviable foods only. Try this better alternative
const _pickFoods = (allFoods) => {
const firstFood = allFoods[Math.floor(Math.random() * allFoods.length)]
const possibleSecondFoods = allFoods.filter(food => food !== firstFood && food.attributes.calories !== firstFood.attributes.calories)
const secondFood = possibleSecondFoods[Math.floor(Math.random() * possibleSecondFoods.length)]
return secondFood
}
Try this:
import Foods from "../foods.json";
class Game extends Component {
state = {
firstFood: Foods[Math.floor(Math.random() * Foods.length)],
secondFood: Foods[Math.floor(Math.random() * Foods.length)],
mostCalories: "placeholder"
};
componentDidMount() {
setTimeout( () => {
while (
// to check that the two foods' calories are not the same
this.state.firstFood.attributes.calories !==
this.state.secondFood.attributes.calories
) {
console.log(this.state.firstFood.attributes.calories);
console.log(this.state.secondFood.attributes.calories);
debugger;
// an infinite loop because the below setState function doesn't run properly
this.setState({
firstFood: Foods[Math.floor(Math.random() * Foods.length)]
});
}
},2000)
}
I think it is because the data is not ready yet.

React native props declaration confusion

I have came across this SendBird With React-Native chatting app and i am studying the code structure of it. The following line of codes is causing me confusion:
Function.js
export const sbAdjustMessageList = (list) => {
return list.map((message, i) => {
message['time'] = sbUnixTimestampToDate(message.createdAt);
message['readCount'] = 0;
if (message.isUserMessage() || message.isFileMessage()) {
message['isUser'] = (message.sender.userId === SendBird.getInstance().getCurrentUserId());
} else {
message['isUser'] = false;
}
if (message.sender) {
message.sender['isShow'] = true;
if(!message.sender.profileUrl) {
message.sender.profileUrl = 'default-image';
}
}
if (i < list.length - 1) {
let prevMessage = list[i + 1];
if (message.isUserMessage() || message.isFileMessage()) {
if (prevMessage.isUserMessage() || prevMessage.isFileMessage()) {
if (prevMessage.sender.userId === message.sender.userId) {
message.sender.isShow = false;
}
}
}
}
return message
});
}
Main.js
_renderFileMessageItem = rowData => {
const message = rowData.item;
if (message.isUserMessage()) {
return <TextItem isUser={message.isUser} message={message.message} />;
} else if (sbIsImageMessage(message)) {
return <ImageItem isUser={message.isUser} message={message.url.replace("http://", "https://")} />;
} else {
return <FileItem isUser={message.isUser} message={message.name} />;
}
};
In Function.js, the is this declaration of message['isUser'] then it is exported to Main.js to be used. However in Main.js, isUser becomes a props to the imported component. Also, there is no initiation of isUser in constructor class.
My question is what does message['isUser'] here means? Is it a javascript thing or a redux features (The sample app uses redux and react-redux)?
So, with the given code, it can be inferred that the function sbAdjustMessageList takes a list i.e. an array of messages and formats all the messages inside it and returns the array of formatted messages while the isUser being a key in each message.
However in Main.js, _renderFileMessageItem is a kind of item renderer for a ListView and each item rendering receives a message. Its is quite clear that the rowData is having a message item inside it and this message is one of the messages from the array which the sbAdjustMessageList function returned (as this array must have been provided to the ListView).
Coming to isUser being a prop for TextItem, its JSX, there is no necessity to provide prop declaration to the components while constructing.Although its always better to write quality code and declare all your prop and types with PropTypes
and message['isUser'] is just javascript's way of manipulating object properties.

Categories

Resources