I have a component called StudentPage which shows the details of a student depending on the current student logged in currentStudent . The currentStudent object doesn't hold all the attributes of the Student model, so I fetch all the students with an axios request and get the student which matches the curresntStudent by id, and get all the attributes I need from it.
Now the problem is when I try to access the value of a student property, I get the error "TypeError: Cannot read property 'name' of undefined".
So I console.log- students, currentStudent, and student and noticed that when I try to access the currentStudent.name (commented below), the console.log-students outputs the list of students from my db, also noticed that the output of console.log-student object matches that of currentStudent. However, when I try to access student.name (code below), the console.log outputs shows an empty array for the students list causing the student object to be undefined.
Can anyone please help explain why I am noticing this behaviour, perhaps some concept of useEffect that I don't yet understand.
const StudentPage=({currentStudent, students, fetchStudents})=>{
useEffect(()=>{
fetchStudents();
},[]) //I tried making currentStudent a dependency [currentStudent] but same behaviour ocurs.
console.log(students);
console.log(currentStudent)
const student= students.filter(pupil=>{return(pupil.id===currentStudent.id)})[0]
console.log(student)
// return (<div>name: {currentStudent.name}</div>) this works normally but currentUser doesn't have all attributes i need
return(
<div>Name: {student.name}</div>
)
const mapStateToProps= state=>{
return {
students: state.students
}
}
export default(mapStateToProps,{fetchStudents})(StudentPage);
Here you can find an example on how to do what you want:
import React from "react";
import { useState, useEffect } from "react";
export function StudentPage({ studentId, fetchStudents }) {
const [student, setStudent] = useState();
useEffect(() => {
(async () => {
const students = await fetchStudents();
const currentStudent = students.find((student) => {
console.log(student);
return student.id === studentId;
});
setStudent(currentStudent);
})();
}, [studentId, fetchStudents]);
return (
<>
Student ID: {studentId}, Student name: {student && student.name}
</>
);
}
useEffect will be triggered whenever the dependency (studentId in this case) changes, including on first render. You should store you value in state (using useState) and have the necessary checks in place to see whether it is null or not.
Related
I'm reusing a couple of external components to create my custom Combobox in strapi app.
Values are received from server so I need to add options dynamically.
Currently there is the following code:
import React, { useState, useEffect } from "react";
import {
Combobox,
ComboboxOption
} from "#strapi/design-system";
export default function ComboboxCustom({
valuesList,
valueSelected
}) {
const [value, setValue] = useState('');
const combo = (<Combobox label="Country" value={value} onChange={setValue}>
{valuesList.map((entry) => {
return(
<ComboboxOption value="{entry.id}">{entry.name}</ComboboxOption>
);
})}
</Combobox>);
// setValue(valueSelected)
return combo;
}
And everything goes good until I try so set 'selected' option basing on another set of data. In static world I could just say useState(valueSelected) and it will work. But as code generated dynamically, there is no related option yet, so I get failure like "Failed to get 'props' property of undefined".
I tried to put this combobox into a variable and set state between creation and returning it (commented setValue line before the return statement) but then app gets in a loop and returns "Too many re-renders".
Does anyone has an idea of how to change/rewrite this to be able to set selected value for dynamically created combobox?
So I assume that the values are dynamically fetched and passed to the ComboboxCustom.
I think you can add setValue(valueSelected) inside an useEffect.
onChange of the prop valueSelected.something like,
useEffect(() => {
setValue(valueSelected)
}, [valueSelected])
Also handle the return when the value is not yet loaded. like before doing valuesList.map, first check if valueList ? (render actual) : (render empty)
Hope this helps!!
Thanks,
Anu
Finally I got working solution based on answer from #Anu.
Cause valuesList is got as GET-request from another hook, I have to check values are already present (first hook hit gives [] yet) and bind Combobox state updating to change of valuesList also. Though I don't fell like this solution is perfect.
import React, { useState, useEffect } from "react";
import {
Combobox,
ComboboxOption
} from "#strapi/design-system";
export default function ComboboxCustom({
valuesList,
valueSelected,
}) {
const [value, setValue] = useState('');
let combo = null;
useEffect(() => {
if(combo && combo?.props?.children?.length > 0 && valuesList.length > 0) {
setValue(valueSelected)
}
}, [valueSelected, valuesList])
combo = (<Combobox label="Country" value={value?.toString()} onChange={setValue}>
{valuesList.map((entry) => {
return(
<ComboboxOption value={entry?.id?.toString()}>{entry.name}</ComboboxOption>
);
})}
</Combobox>);
return combo;
}
After that I decided avoid creating custom component based on already published as I'll need to add and process event listeners that are added for us in the existing components. So I placed this code directly into my modal and it also works:
const [countries, setCountries] = useState([]);
const [deliveryCountryValue, setDeliveryCountryValue] = useState('');
useEffect(async () => {
const countriesReceived = await countryRequests.getAllCountries();
setCountries(countriesReceived);
}, []);
useEffect(() => {
// If there is no selected value yet, set the one we get from order from server
const valueDelivery = deliveryCountryValue != '' ? deliveryCountryValue : order.country?.id;
if(countries.length > 0) {
setDeliveryCountryValue(valueDelivery);
order.country = countries.find(x => x.id == valueDelivery);
}
}, [deliveryCountryValue, countries])
<Combobox key='delivery-combo' label="Country" value={deliveryCountryValue?.toString()} onChange={setDeliveryCountryValue}>
{countries.map((entry) => {
return(
<ComboboxOption key={'delivery'+entry.id} value={entry?.id?.toString()}>{entry.name}</ComboboxOption>
);
})}
</Combobox>
I'm Using React for the first time and I am trying to filter a JSON response so that I can display a list of products.
When I log the result of the filter to the console I seem to get the correct data. However when I try and map over it and render it ".Products" seems to be undefined.
import React, { useContext } from 'react';
import { MenuContext } from '../context/menuContext';
function Category() {
const [categoryItems] = useContext(MenuContext);
const category = categoryItems.filter(element => element.CategoryName == "Rice");
console.log(category);
return (
<div>
<h1>Category Page</h1>
{category.Products.map(productItem => (
<h3 key={productItem.Id}>{productItem.ProductName}</h3>
))}
</div>
);
}
export default Category;
Update: Thanks #Ori Drori for your comment. I have tried to use .find and still does not work.
I think this is something to do with how react works. Becuase in the console I am getting an empty output before I get the correct result.
Update 2: I implemented the answer that I have marked as correct and its now working. HOWEVER. There are two things I don't understand and I would like someone to explain.
When I call
console.log(console.log(category);
Why is it that in the console I see 'undefined' before I get the result with the data.
Why do I need to put 'category &&' when I remove it. It stops working.
category is a list, not a single object. The output of an Array.filter is always a list. That is why: category.Product is undefined.
If you want to find a category with the name Rice, you can change the code to:
import React, { useContext } from "react";
import { MenuContext } from "../context/menuContext";
function Category() {
const [categoryItems] = useContext(MenuContext);
const category = categoryItems.find(
(element) => element.CategoryName === "Rice",
); // change `filter` to `find` and `==` to `===`
console.log(category);
return (
<div>
<h1>Category Page</h1>
{category && // add this because category can be undefined
category.Products.map((productItem) => (
<h3 key={productItem.Id}>{productItem.ProductName}</h3>
))}
</div>
);
}
export default Category;
If it still does not work, show the error and also console.log category.Products
When you used the method filter it returns as an array:
category.map(()=>)
or use the method to find the item product in category:
const find = category.find(elem=>elem.product)
find.map(()=>)
I am trying to get only females from an array using a filter, but on the first attempt react query returns the whole array, after that it is working fine. Any idea what property I have to add or remove, so this side effect disappears.
Here is my code:
import React, { useState } from "react";
import { useQuery } from "react-query";
import getPersonsInfo from "../api/personCalls";
export default function Persons() {
const [persons, setPersons] = useState([]);
const { data: personData, status } = useQuery("personsData", getPersonsInfo, {
onSuccess: (data) => {
setPersons(data.data);
},
onError: (error) => {
console.log(error);
}
});
const getFemaleOnlyHandler = () => {
const result = personData.data.filter(
(person) => person.gender === "female"
);
setPersons(result);
};
return (
<>
<button onClick={getFemaleOnlyHandler}>Female only</button>
{status === "loading" ? (
<div>Loading ... </div>
) : (
<div>
{persons.map((person) => (
<div>
<p>{person.name}</p>
<p>{person.lastName}</p>
<p>{person.address}</p>
<p>{person.gender}</p>
<p>{person.country}</p>
<p>{person.city}</p>
</div>
))}
</div>
)}
</>
);
}
I added the full code in code sandbox: https://codesandbox.io/s/relaxed-drake-4juxg
I think you are making the mistake of copying data from react-query into local state. The idea is that react-query is the state manager, so the data returned by react-query is really all you need.
What you are experiencing in the codesandbox is probably just refetchOnWindowFocus. So you focus the window and click the button, react-query will do a background update and overwrite your local state. This is a direct result of the "copy" I just mentioned.
What you want to do is really just store the user selection, and calculate everything else on the fly, something like this:
const [femalesOnly, setFemalesOnly] = React.useState(false)
const { data: personData, status } = useQuery("personsData", getPersonsInfo, {
onError: (error) => {
console.log(error);
}
});
const getFemaleOnlyHandler = () => {
setFemalesOnly(true)
};
const persons = femalesOnly ? personData.data.filter(person => person.gender === "female") : personData.data
you can then display whatever you have in persons, which will always be up-to-date, even if a background update yields more persons. If the computation (the filtering) is expensive, you can also use useMemo to memoize it (compute it only when personData or femalesOnly changes - but this is likely a premature optimization.
I'm not totally familiar with react-query however the problem is likely that it is re-fetching (async!) everytime the component updates. Since setPersons() triggers an update (ie. sets state) it'll update the new persons state to be the filtered female list and then trigger a fetch of all persons again which comes back and sets the persons state back to the full list (ie. see what happens when you click the female filter button and then just leave it).
There is a more idiomatic way to achieve this in React which is to keep a "single source of truth" (ie. all the persons) and dynamically filter that based on some local ui state.
For example see below where data becomes the source of truth, and persons is a computed value out of that source of truth. This has the benefit that if your original data changes you don't have to manually (read: imperatively) update it to also be females only. This is the "unidirectional data flow" and "reactivity" people always talk about and, honestly, it's what makes React, React.
const { data = { data: [] }, status } = useQuery(
"personsData",
getPersonsInfo,
{
onSuccess: (data) => {},
onError: (error) => {
console.log(error);
}
}
);
const [doFilterFemale, setFilterFemale] = useState(false);
const persons = doFilterFemale
? data.data.filter((person) => person.gender === "female")
: data.data;
https://codesandbox.io/s/vigorous-nobel-9n117?file=/src/Persons/persons.jsx
This is ofc assuming you are always just loading from a json file. In a real application setting, given a backend you control, I would always recommend implementing filtering, sorting and pagination on the server side otherwise you are forced to over-fetch on the client.
http://ddragon.leagueoflegends.com/cdn/10.16.1/data/en_US/champion.json
This is the api im attempting to work with. For some reason when i go to map over it to pull data from it and display the data it says champions.map is not a function. (I am guessing it says this because i cant iterate over an object using .map. So i tried using Object.keys(champions) and i still cant really get it to do what i want it to do.
I feel im missing something super easy as its been quite some time since i have made any api calls.
Here is my code.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import ChampionContainer from "./ChampionContainer";
// import { Link } from 'react-router-dom';
const ChampionList = () => {
const [ champions, setChampions ] = useState([])
console.log("This is our state",champions)
useEffect(() => {
const url = "http://ddragon.leagueoflegends.com/cdn/10.16.1/data/en_US/champion.json";
axios.get(url)
.then(response => {
setChampions(response.data.data);
})
.catch(error => {
console.log("server Error", error)
})
}, [champions]);
return (
// Object.keys(a).map(function(keyName, keyIndex) {
// // use keyName to get current key's name
// // and a[keyName] to get its value
// })
<div>
{Object.keys(champions).map((key) => {
return (<div key={key}>{champions[key]}</div>)
})}
{/* {champions.map((champion) => (
<ChampionContainer key={champion.id} champion={champion}/>
))} */}
</div>
);
}
export default ChampionList;
Thanks in advance for the help.
You are right, you're first attempt at doing .map() on champions wont work as it is not an array, it's an object.
You're second attempt is closer, though when you do -
champions[key]
this returns one of your champion objects. You just need to access the property you want from that object, e.g.
champions[key].name - to get the name of the champion e.g. Aatrox
You have a infinite loop there.
In useEffect pass champions as a parameter, when it changes you excecute useEffect, but in there you setChampions, then champions changes an useEffect is running again. infinite loop as a result.
You debug your program to make sure that respomse.data.data have the information you need?
In setChampions function is better to use setChampions(current => current + newData);
this is an object, so you should use spread operator to map onto a new object.
import React, { useState, useEffect } from "react";
import { checkRef } from "./firebase";
function Dashboard() {
const [count, setCount] = useState([]);
let hi = [];
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
checkRef.on("value", snapshot => {
let items = snapshot.val();
let newState = [];
for (let tracker in items) {
newState.push({
reason: items[tracker].reason,
teacher: items[tracker].teacher
});
}
setCount({ items: newState });
});
// Update the document title using the browser API
// checkRef.on('value',(snapshot) => {
// console.log(snapshot.val())
// })
}, []);
return (
<div>
{count.items.map(item => {
return <h1>{item.reason}</h1>;
})}
</div>
);
}
export default Dashboard;
I am trying to return each item as an h1 after getting the item but i keep getting the error ×
TypeError: Cannot read property 'map' of undefined. I apoligize i AM new to web dev and trying to learn. I have spent way to much time with no results. thanks
Problem lies with your initalization of count.
For the very first render your count state variable doesn't contain any property named items. hence it fails.
your state variable is getting items prepery only after useEffect which excutes after first render.
based on your useeffct code, you should initialize count state variable with an object like follwing,
const [count, setCount] = useState({items:[]});
It sounds like useEffect is not calling the call back function (the one you defined).
You could try initializing the count.items to be an array
Or make sure that useEffect calls the callback function
Another way to fix your issue is simply do this:
{
count && count.items && count.items.map( item => {
// do something with the item
})
}