Hello guys I made a history page so the user can look at their past search history and so I used localstorage. On the history page, I am trying to render data that stays there and isn't changed when I go to search the api again. Instead I want it to keep adding data to the page. I was thinking the data would just keep being added to the new array in local storage but it overwrites the existing data with new data. Ive made an attempt to prevent this but I am stuck.
Here is my code of all of my pages
Search page
export default function SearchPage(props) {
// creating state to fetch the api
const [search, setSearch] = useState('')
// this is my function to monitor the words that are put into the input so these keywords that only matches specific data
// in the api and so one the data is fetched it renders the topic related to that keyword
const handleSearch = (event) => {
setSearch(event.target.value)
}
const handleSubmit = (event) => {
event.preventDefault();
// this is where I bring my useState variable to monitor the state of the key words in order to
// target specific data from the api
let url = `http://hn.algolia.com/api/v1/search_by_date?query=${search}`;
axios
.get(url)
.then((response) => {
const result = response.data.hits;
// this pushes the data fetched from the api to the results page using history
props.history?.push ({
pathname: '/results',
state: result,
});
})
// catching any errors to let me know if there is something going on in the .then function
.catch((error) => {
console.log(error)
console.log('Error while fetching data!')
})
}
return (
<div>
<div className="search-form-container">
{/* my form in where I search data by keywords */}
<form onSubmit={handleSubmit}>
<input type='text' placeholder="search" onChange={handleSearch} value={search}/>
<button type="submit">Search</button>
</form>
<hr/>
<Link to="/history">Your Search History</Link>
</div>
</div>
)
}
Results page
export default function ResultsPage(props) {
console.log(props)
// the variable declared below is where I am bringing in the data through history from the api.
const data = props.history.location.state;
let storedData = localStorage.setItem('data', JSON.stringify(data))
console.log(storedData)
// being that I am using history, I can map through the array of objects on the results page as shown below and then render it
const hitList = data.map((data, idx) => {
return (
<ul key={idx}>
<div>
<li> Author: {data.author}</li>
<li>Story Title: {data.story_title}</li>
<li>Comment: {data.comment_text}</li>
<li>Created on: {data.created_at}</li>
<li></li>
</div>
<hr/>
</ul>
)
})
return (
<div>
<Link to='/'><h1>Search</h1></Link>
<Link to='/history'><h1>Your History</h1></Link>
{/* In order for me to show my data I first had to map through the array of objects and then put the variable "hitlist" in the return */}
<h3>{hitList}</h3>
</div>
)
}
History page
export default function SearchHistoryPage(item) {
const storedData = JSON.parse(localStorage.getItem('data'));
storedData.push(item);
localStorage.setItem('data', JSON.stringify(storedData));
console.log(storedData);
const searchHistory = storedData.map((data, idx) => {
return (
<ul key={idx}>
<li> Author: {data.author}</li>
<li>Story Title: {data.story_title}</li>
<li>Comment: {data.comment_text}</li>
<li>Created on: {data.created_at}</li>
</ul>
)
})
return (
<div>
<h2>{searchHistory}</h2>
</div>
)
}
In your Results page, you are overwriting your localStorage 'data' key with only the results fetched from Search page.
What you can do is to fetch your "history" (localStorage 'data') before you push the new results in your Results page, and not in your History page as so:
In Results page:
const data = props.history.location.state;
// ADD THESE 2 LINEs
const historicData = JSON.parse(localStorage.getItem('data'));
historicData.push(data)
// Store "historicData" instead of "data"
// let storedData = localStorage.setItem('data', JSON.stringify(data))
let storedData = localStorage.setItem('data', JSON.stringify(historicData))
console.log(storedData)
In History page:
// Just use localStorage 'data'.
// Do not modify anything in History page since you are not taking any actions here.
const storedData = JSON.parse(localStorage.getItem('data'));
storedData.push(item);
// localStorage.setItem('data', JSON.stringify(storedData)); <-- REMOVE
// console.log(storedData); <-- REMOVE
Related
I'd like to dynamically append my empty array using useState. I want the program to work in such a way that on each button click, it fetches new data and appends it to the already existing array. How do I do this. The api I'm fetching automatically has new data upon refreshing the page.
import "./styles.css";
import React, { useState, useEffect } from "react";
export default function App() {
const[data, setData] = useState([]);
let info = [];
const handleFetch = () => {
fetch("https://www.randomuser.me/api")
.then((response) => response.json())
.then((data) => setData(data.results));
}
const information = data.map((details) => {
return (
<>
<h1> {details.name.title} </h1>
<h1> {details.name.first} </h1>
<h1> {details.name.last} </h1>
<img src = {details.picture.large}/>
</>
)
})
return (
<div className="App">
<button onClick = {handleFetch}>Fetch New Details</button>
<div>{information}</div>
</div>
);
}
First, don't shadow the variable. Use a different variable name in your API response handler so you can access both. So instead of this:
.then((data) => setData(data.results));
Something like this:
.then((newData) => setData(newData.results));
Then you can merge the two arrays when setting state. For example, you can concat the arrays:
setData(data.concat(newData.results))
I have code like this below and my question is what can I do to get the products fetched before the components are rendered? I mean in my Panel component I have a .map function which shows an error "Cannot read property 'map' of undefined" because products were not fetched quickly enough.
const Announcments = () => {
const [announcements, setAnnouncement] = useState([]);
useEffect(() => {
const getProducts = async () => {
const res = await fetch("http://localhost:8000/api/products", {
method: "GET",
});
const data = await res.json();
await setAnnouncement(data);
};
getProducts();
}, []);
return (
<div className="announcments">
<TopNav results={announcements.length} />
<Panel announcements={announcements} />
<div className="down-pages-list">
<PagesList />
</div>
</div>
);
};
.map function:
{announcements.results.map((announcement) => (
<SingleAnnoun
price={announcement.price}
title={announcement.name}
photo={getPhoto(announcement.images[0].file_name)}
/>
))}
Issue
The issue here isn't that "products were not fetched quickly enough" but rather that your initial state doesn't match what you are attempting to render on the initial render. I.e. announcements is initially an empty array ([]) but you are attempting to map announcements.results which is obviously undefined.
Solution
Use valid initial state so there is something to initially render.
const [announcements, setAnnouncement] = useState({ results: [] });
Now announcements.results is defined and mappable.
This also assumes, of course, that your setAnnouncement(data); state update is also correct. i.e. that your JSON data is an object with a results property that is an array.
BTW, setAnnouncement is a synchronous function so there is nothing to await for.
I am building Weather App, my idea is to save city name in database/localhost, place cities in useState(right now it's hard coded), iterate using map in first child component and display in second child component.
The problem is that 2nd child component outputs only one element (event though console.log prints both)
BTW when I change code in my editor and save, then another 'li' element appears
main component
const App = () => {
const [cities, setCities] = useState(['London', 'Berlin']);
return (
<div>
<DisplayWeather displayWeather={cities}/>
</div>
)
}
export default App
first child component
const DisplayWeather = ({displayWeather}) => {
const [fetchData, setFetchData] = useState([]);
const apiKey = '4c97ef52cb86a6fa1cff027ac4a37671';
useEffect(() => {
displayWeather.map(async city=>{
const res =await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData([...fetchData , data]);
})
}, [])
return (
<>
{fetchData.map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
}
export default DisplayWeather
second child component
const Weather = ({data}) => {
console.log(data) // it prints correctly both data
return (
<li>
{data.name} //display only one data
</li>
)
}
export default Weather
The Problem
The setFetchData hooks setter method is asynchronous by default, it doesn't give you the updated value of the state immediately after it is set.
When the weather result for the second city is returned and set to state, the current value fetchData at the time is still an empty array, so you're essentially spreading an empty array with the second weather result
Solution
Pass a callback to your setFetchData and get the current previous value of the state and then continue with your spread accordingly.
Like this 👇🏽
setFetchData((previousData) => [...previousData, data]);
I'm try to get single document as detail information from Firebase database under collection "books", however my array method map does not recognize as function due to the render produce "undefined". Somehow render again and produce the object value in log. I posted the screenshot of the log above, hoping somebody help me out, thanks!!!!!
import React, {useState, useEffect} from 'react'
import {Link} from 'react-router-dom'
import firebase from '../config/fbConfig'
const BookDetails = (props) => {
const [books, setBooks] = useState([])
useEffect(() => {
const db = firebase.firestore()
const id = props.match.params.id
var docRef = db.collection("books").doc(id);
docRef.get().then(doc => {
if(doc.exists){
const data = doc.data()
console.log("Document data:", data)
setBooks(data)
}else {
console.log("No such document!");
}
}).catch(error => {
console.log("Error getting document:", error);
})
}, [])
console.log('this log is before return', books.title)
return (
<div className="book_details">
<Link to="/"><h2>Home</h2></Link>
{console.log("this log is in the return method", books.title)}
<h1>The Summary Of the Book </h1>
{books.map( book => <ul key = "book.id" >
<li>Book Title: {book.title}</li>
<li>Book Author: {book.author}</li>
<li>Book Summery: {book.brief}</li>
</ul>)}
</div>
)
}
export default BookDetails
Because you are testing whether books is undefined and only call the map function if it is defined (i.e. {books && books.map( [...] )}), the problem must lie somewhere else.
You are fetching a single document from your Firebase database. Therefore, the returned data will not be an array but an object which does not have the map function in its prototype. You can verify this from your console logs.
Your component renders twice because you are changing its state inside the useEffect via setBooks(data).
const db = firebase.firestore()
const id = props.match.params.id
First of all move these lines inside of useEffect.
Coming to the problem
You are fetching a single doc(object) from firebase and saving it in a state which is an array. Change your useState to
const \[book, setBook\] = useState(undefined) // or useState({})
Change your return to
return (
<div className="book_details">
<Link to="/"><h2>Home</h2></Link>
{console.log("this log is in the return method", books.title)}
<h1>The Summary Of the Book </h1>
{book && <div key={book.id}> {book.brief} </div>}
</div>
)
// or {Object.keys(book).length !== 0 && <div key={book.id}> {book.brief} </div>}
if you have used empty object in useState.
I am trying to go into my Firebase database and save all groups that a user belongs to to an array then push that array to the components state and render a list of items accordingly but my render is not showing the items from the state.
componentDidMount(){
this.setGroups();
}
setGroups(){
const user = fire.default.auth().currentUser.uid;
const dbRef = fire.default.database();
const currentUserRef = dbRef.ref('users').child(user);
const userGroups = currentUserRef.child('groups');
const groupsRef = dbRef.ref('groups');
const idArray = []
const groupArray = []
userGroups.on('value', snapshot => {
snapshot.forEach(groupId => {
idArray.push(groupId.val().groupId)
})
idArray.forEach(function(group, i) {
groupsRef.child(idArray[i]).on('value', snap =>{
let addGroup = {
id: snap.key,
name: snap.val().groupName,
description: snap.val().groupDescription,
};
groupArray.push(addGroup);
})
this.setState({
hasGroups: true,
groupsHold: groupArray,
})
}.bind(this))
})
}
render(){
console.log(this.state.hasGroups)
if(this.state.hasGroups){
return(
<div>
<ul>
{this.state.groupsHold.map((group, i) =>
<ListItem button
selected={this.state.selectedIndex === group}
onClick={event => this.handleListItemClick(event, group, i)}
key={`item-${group.id}`}>
<ListItemText primary={`${group.name}`} />
</ListItem>
)}
</ul>
</div>
)
}
return(
<div>
<p>You have no groups yet</p>
</div>
)
}
I'm setting the groups by going into my firebase and grabbing all groups that a user is part of from their user node then iterating through the 'groups' node and saving all groups that the user is a part of from that to the state. I was expecting the render to reload on the new state but It is showing up blank. It's not showing the <p>You have no groups yet</p> so it recognizes that the state has groups but is is just showing an empty <ul>.
Calls to Firebase are asynchronous, but setGroups is not an async function as you have it written. I think your problem will be fixed if you make setGroups an async function and await your call to userGroups.on('value', () => {}) inside of it.
Currently, your component is setting state when groupsArray is empty. So even though this.state.hasGroups is true, there are no groups to render!