React: Accessing an objects properties within an array that I have mapped - javascript

I'm trying to make my first real React app and am pulling information from a database, updating the state to set that info as an array, and then trying to access the properties of the objects in the array.
function App() {
const [students, setStudents] = useState([]);
function fetchStudentInfo() {
fetch('https://api.hatchways.io/assessment/students')
.then(response => {
return response.json();
})
.then(data => {
const transformedStudentData = data.students.map(studentData => {
return {
imgURL: studentData.pic,
fullName: `${studentData.firstName} ${studentData.lastName}`,
email: studentData.email,
company: studentData.company,
skill: studentData.skill
}
});
setStudents(transformedStudentData);
});
}
fetchStudentInfo();
return (
<div> {console.log(students[0].fullName)}
<h1>Student Assessments</h1>
</div>
)
}
export default App;
I know I shouldn't be console.logging the info I'm trying to get, but I'm not even sure how to use the console in React to find out how to access my variables. Anyway, I get "TypeError: Cannot read properties of undefined (reading 'fullName')" as an error and it fails.
I'm really trying to pass down the array as properties to be used in my components, but I've taken out code to try and simplify the problem for myself and this is where I've hit a wall.

On the first render your students state is an empty array because that's how you've initialised it. On that first render you can't access the first index (because the data hasn't been fetched, or the state updated), and you can't access a property of an element that doesn't exist yet.
So you need a condition in there that renders: 1) if students.length is zero return a loading message (for example) 2) otherwise map over the students array which you now know exists to produce a list of student names.
Here's a working example that uses useEffect to add an array to state after three seconds simulating your API fetch. You should be using useEffect like this for your fetch (with an empty dependency array) instead of calling your function directly in the component.
const { useEffect, useState } = React;
const json = '[{ "fullName": "Bob" },{ "fullName": "Karen" },{ "fullName": "Rick" }]';
function mockApi() {
return new Promise((res, rej) => {
setTimeout(() => res(json), 3000);
});
}
function Example({ data }) {
const [ students, setStudents ] = useState([]);
useEffect(() => {
function getData() {
mockApi()
.then(json => JSON.parse(json))
.then(data => setStudents(data));
}
getData();
}, []);
// Initial log will be `[]`
// Second log will be the updated state stringified
useEffect(() => {
console.log(JSON.stringify(students));
}, [students]);
// No data, so return the loading message, or spinner
if (!students.length) return <div>Loading: wait 3 seconds</div>
// Data! `map` over it to produce the list
return (
<ul>
{students.map(student => {
return <li>{student.fullName}</li>;
})}
</ul>
);
};
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Related

Why does firebase return undefined in my component but I can see the object in console.log when I read my database?

Im using Next.js and Typescript.
I'm trying to implement a custom hook for reading my data and outputting the object. I'm using the firebase realtime database. My data is an array of objects. Firebase returns an object of objects with .val() which I then convert to an array of objects in the custom hook, but I cannot pass it to my component for some reason. I can console.log the new array of objects, but can't return it to my component which reads undefined when I console.log it at the component.
I notice that sometimes when I edit the code and it auto-reloads, the console.log for my component works.
Here's my custom hook snippet and component trying to call it. Thanks!
Custom Hook Snippet:
// ... rest of hook is above this snippe (the hook is useFirebaseAuth())
const readBlogData = () => {
const db = getDatabase(app);
const dbRef = ref(db, "blogs");
let newArrObj;
onValue(dbRef, (snapshot) => {
newArrObj = Object.values(snapshot.val());
console.log(newArrObj); // I can see the array of objects in console.log
});
return newArrObj;
};
return { login, logout, loginStatus, writeBlogData, readBlogData };
}
Component:
export default function BlogSection() {
const { readBlogData } = useFirebaseAuth();
console.log(readBlogData()); // console.log shows undefined
return (
<section className={styles.blogSection}>
<Card className={styles.blogsContainer}>
<h5>Blogs</h5>
<Paginate blogs={DUMMY_DATA} />
</Card>
</section>
);
}
The code in the callback for onValue happens after the return newArrObj line.
You probably want to set it on state in your hook and return that state so the consumer can react to it.
const [blogs, setBlogs] = useState([]);
// then
onValue(dbRef, (snapshot) => {
newArrObj = Object.values(snapshot.val());
setBlogs(newArrObj);
});
// then
return { login, logout, loginStatus, writeBlogData, readBlogData, blogData: blogs };
Program the consumer to fetch the data and display the response
const Consumer = () => {
const { blogData, readBlogData } = useWhatever();
useEffect(() => readBlogData(), []); // Tell the hook to get some data
// blogData will initially be an empty array
// and then eventually will have some elements
return blogData.length ? <p>{JSON.stringify(blogData)</p> : null;
};

In React, how can I make a single api call, set the result to a state variable, and display it?

I am programming a react application where I need to make a single async api call, save the result to a state variable and display the result. I am using an axios instance where the response of the api call is a nested object. For example:
{
kanji:
{character: "", stroke:""},
quote: "string"
}
So far I have the following code where I am able to console.log the homeData object successfully. However I get the error: 'TypeError: homeData is undefined'.
const Home = () =>{
const [homeData, setHomeData] = useState({})
const getHomeData = async()=>{
instance.get("/data")
.then(res =>{
setHomeData(res.data)
})
.catch(error =>{
console.log(error)
})
}
useEffect(()=>{
getHomeData()
}, [])
console.log(homeData)
return(
<>
<p>{homeData}<p>
</>
)
}
This seems like a promise issue but I am not sure how to fix it. Thank you for your help in advance.
This is not a promise issue, it has to due with the order in which the lifecycle methods run. useEffect runs after the component renders. This means when the component first renders, the homeData is an empty object. homeData will be present in the second render. The following code first checks if homeData exists, then if it exists, it displays the kanji character. Note also, you cant just display a raw object into the dom so you will have to access the homeData by property for it to display
const Home = () =>{
const [homeData, setHomeData] = useState({})
const getHomeData = async()=>{
instance.get("/data")
.then(res =>{
setHomeData(res.data)
})
.catch(error =>{
console.log(error)
})
}
useEffect(()=>{
getHomeData()
}, [])
console.log(homeData)
return(
<>
<p>{homeData && homeData.kanji.character}<p>
</>
)
}

Saving api response to State using useState and Axios (React JS)

I'm having an issue when trying to save to State an axios API call. I've tried
useState set method not reflecting change immediately 's answer and many other and I can't get the state saved. This is not a duplicate, because I've tried what the accepted answer is and the one below and it still doesn't work.
Here's the (rather simple) component. Any help will be appreciated
export const Home = () => {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
axios
.get('/call-to-api')
.then((response) => {
const data = response.data;
console.log(data); // returns correctly filled array
setWidgets(widgets, data);
console.log(widgets); // returns '[]'
});
}, []); // If I set 'widgets' here, my endpoint gets spammed
return (
<Fragment>
{/* {widgets.map((widget) => { // commented because it fails
<div>{widget.name}</div>;
})} */}
</Fragment>
);
};
Welcome to stackoverflow, first thing first the setting call is incorrect you must use spread operator to combine to array into one so change it to setWidgets([...widgets, ...data]); would be correct (I assume both widgets and data are Array)
second, react state won't change synchronously
.then((response) => {
const data = response.data;
console.log(data); // returns correctly filled array
setWidgets(widgets, data);
console.log(widgets); // <--- this will output the old state since the setWidgets above won't do it's work till the next re-render
so in order to listen to the state change you must use useEffect hook
useEffect(() => {
console.log("Changed Widgets: ", widgets)
}, [widgets])
this will console log anytime widget changes
the complete code will look like this
export const Home = () => {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
axios
.get('/call-to-api')
.then((response) => {
const data = response.data;
setWidgets([...widgets, ...data])
});
}, []);
useEffect(() => {
console.log("Changed Widgets: ", widgets)
}, [widgets])
return (
<Fragment>
{/* {widgets.map((widget) => { // commented because it fails
<div>{widget.name}</div>;
})} */}
</Fragment>
);
};
Try:
setWidgets(data);
istead of
setWidgets(widgets, data);
Your widgets.map() probably fails because there isn't much to map over when the component is being rendered.
You should update it with a conditional like so, just for clarity:
widgets.length>0 ? widgets.map(...) : <div>No results</div>
And your call to setWidgets() should only take one argument, the data:
setWidgets(data)
or if you want to merge the arrays use a spread operator (but then you need to add widgets as the dependency to the useEffect dependency array.
setWidgets(...widgets, ...data)
You might also have to supply the setWidgets hook function to the useEffect dependency array.
Let me know if this helps..

React Component not rendering a passed props

I'm trying to pass an array of users as a props to a component, when I change something and click save, the array is showed in the component but when I hit refresh, the array is disappeared, here is my code:
First in my App.js I'm reading an array of users from the database (works perfectly shows the list of users) :
const [users,setUsers] = React.useState([]);
React.useEffect( () => {
async function fetchData() {
await axios.get('/api/users/')
.then(response => {
setUsers(response.data);
});
}
fetchData();
}
, []);
Then, also in App.js, I'm rendering a ListComponent that takes the users array and shows the users:
return (
<ListComponent users={users} />
);
}
In my ListComponent after a page refresh the console.log shows an empty array []
const ListComponent = (props) => {
React.useEffect(()=>{
console.log(props.users); // []
},[])
When you refresh the page, the ListComponent will be remounted, and what you are logging, is the state of the component just after it is mounted, so the user array is not already fetched. If you want log it when it is fetched, you should add the user array in the dependency array of the useEffect function:
const ListComponent = (props) => {
React.useEffect(()=>{
console.log(props.users); // Should be an empty array first, then updated if your fetch function is working properly
},[props.users]);
// ...
};
If you still cannot see the user array, it means that something is not happening as expected in your fetchData function I guess.

How can I update state as an array of objects without overriding previous state

I want to update setTopic without overriding previous state. But I am getting topic is not iterable error.
What I tried?
I tried looking a different examples on stack overflow, But still couldn't figure out how to append updated state without losing previous state.
Also, which is a better way to save multiple topics : an array of objects, simply objects or simply array?
const AddTopic = (props) => {
const { subjectName } = props;
const [topic, setTopic] = useState([
{
topics: [
{
id: Math.random().toString(36).substr(2, 7),
topicName: "topic name",
subject: subjectName,
},
],
},
]);
const addTopicHandler = () => {
setTopic(
[...topic].map((item) => {
return {
...item,
id: Math.random().toString(36).substr(2, 7),
topicName: "another topic name",
subject: subjectName,
};
})
);
};
console.log(topic);
Instead of the child component using state lift the state to a parent component, and then just create dumb Topic components from the state.
Rename your state. Call it topics, and the update function setTopics. Initialise it as an array, not an array containing one object containing an array.
You can't immediately log an updated state. You need to use useEffect to watch for changes in state, and then log something.
const { useEffect, useState } = React;
// Topic component - just gets handed the
// subject (in this example) in the props
function Topic({ subject, category }) {
return <div>{subject}: {category}</div>;
}
function Example() {
// Initialise `topics` as an array
const [ topics, setTopics ] = useState([]);
// When `topics` is updated, log the updated state
useEffect(() => console.log(JSON.stringify(topics)), [topics]);
// Helper function that maps over the state and
// produces an array of topics
function getTopics() {
return topics.map(topic => {
const { subject, category } = topic;
return (
<Topic
subject={subject}
category={category}
/>
);
});
}
// Helper function to add a new topic object
// to the topics state
function addTopic() {
const obj = { subject: 'Math', category: 'Fish' };
setTopics([...topics, obj ]);
}
return (
<div>
<div>{getTopics()}</div>
<button onClick={addTopic}>Add topic</button>
</div>
);
};
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Try to replace [...topic].map with [...topic.topics].map.
edit
Sorry, I didn't see that the topic itself is an arr of objs, so it should be: [...topic[0].topics].map.

Categories

Resources