I'm building a really basic weather app with React hooks, codesandbox.io and OpenWeatherAPI.
It's breaking at const temp = forecast.main.temp; and returning "TypeError: Cannot read properties of undefined (reading 'temp')" It actually does run when I initially write it but breaks if I add another variable or have to edit the temp variable in any way.
import React, { useState } from "react";
import "../styles.css";
import CityCard from "./cityCard";
export default function GetWeather() {
const [city, setCity] = useState('');
const [forecast, setForecast] = useState([]);
const getCity = async (e) => {
e.preventDefault();
//API variables
const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&units=imperial&appid=${APIkey}`;
//fetch response
try {
const res = await fetch(url);
const data = await res.json();
setForecast(data);
} catch (err) {
console.error(err);
}
};
return (
//basic input form to search by city
<div>
<h1 className="title">Weather App</h1>
<CityCard forecast={forecast} />
<form className="form" onSubmit={getCity}>
<label className="label" htmlFor="city">
<p>Search by City</p>
</label>
<input
className="input"
type="text"
name="query place"
placeholder="i.e. Seattle"
value={city} onChange={(e) => setCity(e.target.value)}
>
</input>
<button
className="button"
type="submit"
>Search</button>
</form>
</div>
);
}
CityCard
import React from "react";
import "../../src/styles.css";
export default function CityCard({ forecast }) {
//pass props to cards
const cityName = forecast.name;
const temp = forecast.main.temp;
return (
<div className="card-container">
<p className="card-title">{cityName}</p>
<p>Temp: {temp} </p>
</div>
);
}
Issue
The initial forecast state is declared as an array:
const [forecast, setForecast] = useState([]);
and passed to CityCard on forecast prop:
<CityCard forecast={forecast} />
then accessed as if it were an object:
function CityCard({ forecast }) {
//pass props to cards
const cityName = forecast.name; // OK, undefined
const temp = forecast.main.temp; // Not OK, can't access temp of undefined
return (
<div className="card-container">
<p className="card-title">{cityName}</p>
<p>Temp: {temp}</p>
</div>
);
}
Solution
Use Optional Chaining operator to protect against null/undefined property accesses.
function CityCard({ forecast }) {
//pass props to cards
const cityName = forecast?.name;
const temp = forecast?.main?.temp;
return (
<div className="card-container">
<p className="card-title">{cityName}</p>
<p>Temp: {temp}</p>
</div>
);
}
Or wait to render CityCard when there's a valid forecast data to display.
const [forecast, setForecast] = useState();
...
{forecast?.name && forecast?.main?.temp && <CityCard forecast={forecast} />}
OpenWeather JSON API response
Related
Below are the two mentioned cases when console logging a state results in different outputs.
CASE1: Console logging currentTitle state logs the previous state even after updating state in titleChangeHandler function.
See Case1 Console Log
import "./ExpenseForm.css";
const ExpenseForm = (props) => {
const [currentTitle, setCurrentTitle] = useState("");
const [currentAmount, setCurrentAmount] = useState("");
const [currentDate, setCurrentDate] = useState("");
const titleChangeHandler = (event) => {
setCurrentTitle(event.target.value);
console.log(currentTitle);
};
const amountChangeHandler = (event) => {
setCurrentAmount(event.target.value);
};
const dateChangeHandler = (event) => {
setCurrentDate(event.target.value);
};
const submitHandler = (event) => {
event.preventDefault();
const expenseData = {
title: currentTitle,
amount: currentAmount,
date: currentDate,
};
props.onSaveExpenseData(expenseData);
setCurrentAmount("");
setCurrentTitle("");
setCurrentDate("");
};
return (
<form onSubmit={submitHandler}>
<div className="new-expense__controls">
<div className="new-expense__control">
<label>Title</label>
<input
type="text"
value={currentTitle}
onChange={titleChangeHandler}
/>
</div>
<div className="new-expense__control">
<label>Amount</label>
<input
type="number"
min="0.1"
step="0.1"
value={currentAmount}
onChange={amountChangeHandler}
/>
</div>
<div className="new-expense__control">
<label>Date</label>
<input
type="date"
min="2019-01-01"
max="2022-12-31"
value={currentDate}
onChange={dateChangeHandler}
/>
</div>
</div>
<div className="new-expense__actions">
<button type="submit">Add expense</button>
</div>
</form>
);
};
export default ExpenseForm;
CASE 2: When we pass a function(onSelectYear) to a child component using props, and call setState function(setYear) inside the passed function, when invoked from child component(ExpensesFilter), then console logging the state shows the latest value after updating.
See Case2 Console Log
// Expenses.js
import "./Expenses.css";
import ExpenseItem from "./ExpenseItem";
import React, { useState } from "react";
import ExpensesFilter from "./ExpenseFilter";
import Card from "../UI/Card";
const Expenses = (props) => {
const [year, setYear] = useState("2020");
const onSelectYear = (year) => {
setYear(year);
console.log(year);
};
return (
<div>
<Card className="expenses">
<ExpensesFilter
selectedYear={year}
onSelectYear={onSelectYear}
></ExpensesFilter>
<ExpenseItem
title={props.expenses[0].title}
amount={props.expenses[0].amount}
date={props.expenses[0].date}
></ExpenseItem>
<ExpenseItem
title={props.expenses[1].title}
amount={props.expenses[1].amount}
date={props.expenses[1].date}
></ExpenseItem>
<ExpenseItem
title={props.expenses[2].title}
amount={props.expenses[2].amount}
date={props.expenses[2].date}
></ExpenseItem>
<ExpenseItem
title={props.expenses[3].title}
amount={props.expenses[3].amount}
date={props.expenses[3].date}
></ExpenseItem>
</Card>
</div>
);
};
export default Expenses;
// ExpenseFilter.js
import React from "react";
import "./ExpenseFilter.css";
const ExpensesFilter = (props) => {
const onSelectChange = (event) => {
props.onSelectYear(event.target.value);
};
return (
<div className="expenses-filter">
<div className="expenses-filter__control">
<label>Filter by year</label>
<select value={props.selectedYear} onChange={onSelectChange}>
<option value="2022">2022</option>
<option value="2021">2021</option>
<option value="2020">2020</option>
<option value="2019">2019</option>
</select>
</div>
</div>
);
};
export default ExpensesFilter;
I understand that react schedules the state changes and this operation is async in nature. Thats why an immediate console log doesn't reflect updated value. But in the second case, we are essentially doing the same. Can anyone please explain, what makes the second case different from first?
If we look at this part of the code:
const [year, setYear] = useState("2020");
const onSelectYear = (year) => {
setYear(year);
console.log(year);
};
The year in console.log(year) is the value that is being passed as an argument to the function onSelectYear
const [year /* this is out of scope for onSelectYear */ , setYear] = useState("2020");
const onSelectYear = (year /* this value */) => {
setYear(year);
console.log(year); // is being logged here
};
On changing the function definition like so:
const [year /* this is now in scope for onSelectYear */ , setYear] = useState("2020");
const onSelectYear = (newYear /* changed the name */) => {
setYear(newYear);
console.log(year); // the year as defined by useState is now being logged here
};
The same observation will be seen as Case 1.
I am new to React. I am stuck on this problem for days now.
I have got a parent component which wraps two sibling components, "FileUpload" and "Documents"
The "FileUpload" is for uploading a file and "Documents" is for displaying all the uploaded files.
I want the "Documents" rerender after a new file is uploaded via "FileUpload", so that it shows the new file in the UI.
What would be the best approach to achieve this ?
Below is the code I have written so far for the sibling components:
FileUpload:
import React, { useState } from "react";
import Axios from "axios";
const FileUpload = (props) => {
const [files, setFiles] = useState([]);
const onInputChange = (e) => {
setFiles(e.target.files);
};
const handleSubmit = async (e) => {
e.preventDefault();
const data = new FormData();
for (let i = 0; i < files.length; i++) {
// console.log(files);
data.append("file", files[i]);
}
data.append("parentDbId", props.parentDbId);
data.append("parentObject", props.parentObject);
//console.log(data);
try {
await Axios.post("http://localhost:5000/upload", data);
} catch (err) {
console.error(err.message);
}
};
return (
<form
// action="http://localhost:5000/upload"
// method="POST"
//encType="multipart/form-data"
onSubmit={handleSubmit}
>
<div className="row mb-3">
<div className="col-lg-4">
<label htmlFor="formFileMultiple" className="form-label mb-0">
Add files
</label>
<input
className="form-control"
type="file"
id="formFileMultiple"
name="file"
multiple
onChange={onInputChange}
/>
</div>
<div className="col-lg-4 mt-0 gx-0">
<button type="submit" className="btn btn-primary mt-4">
Upload
</button>
</div>
</div>
</form>
);
};
export default FileUpload;
====================================================================
Documents:
import React, { useState, useEffect } from "react";
import axios from "axios";
const Documents = (props) => {
const parentDbId = props.parentDbId;
const [documents, setDocuments] = useState([]);
//mount
useEffect(() => {
console.log("first use effect");
loadDocuments();
}, []);
const loadDocuments = async () => {
const result = await axios.get(
`http://localhost:5000/documents/${parentDbId}`
);
setDocuments(result.data);
};
return (
<>
<div className="row">
{documents.map((document, index) => (
<div className="col-lg-3" key={index}>
<a href={document.filePath}>{document.fileName}</a>
</div>
))}
</div>
</>
);
};
export default Documents;
Thanks,
Jimmy
Simple, just have the parent control document state and pass the state and callback down to the children as a prop. Now the siblings are referencing the same state and will be re-rendered when props (ie document state) changes. The parent can also handle the data fetching and uploading.
it will look like this:
const Parent = () => {
const [documents, setDocuments] = useState([]);
...do data fetching here
const handleSubmit = useCallback(async () => {}, []); // You might want to reset document state here?
return (
<div>
<Docs documents={documents} />
<Upload onUpload={setDocuments} onSubmit={handleSubmit} />
</div>
);
}
I wonder if you should actually have two documents components, one for displaying the files being uploaded, and one for displaying the already uploaded files. You would embed one within the Upload component and the other would fetch documents from the api every time onUpload completes
I want to fetch the data when the button is clicked but the Newsitem component is running first and then updating the value of data_grabber. That means it is displaying the defalut values rather than the data that I fetched from the newsapi. After displaying the newsitem component with default values, data_grabber is updating the fetched data.
What can be the solution?
App.js
function App() {
const [input_data, setInput_data] = useState("");
const [btn_data, setBtn_data] = useState("");
const [data_grabber, setData_grabber] = useState([]);
return (
<>
<Navbar
input_data={input_data}
setInput_data={setInput_data}
setBtn_data={setBtn_data}
btn_data={btn_data}
data_grabber={data_grabber}
setData_grabber={setData_grabber}
/>
{data_grabber? data_grabber.map((news_data)=>{
return(
<NewsItem news_data={news_data}/>
)
}):<div>No data available</div>}
</>
);
}
export default App;
Navbar.js
import { useEffect } from "react";
export default function Navbar(props) {
const onClicker = async (e) => {
e.preventDefault();
props.setBtn_data(props.input_data);
};
useEffect(() => {
const fetcher = async () => {
const link = `https://newsapi.org/v2/everything?q=${props.btn_data}&apiKey=API_KEY`;
const raw_data = await fetch(link);
const data = await raw_data.json();
console.log(data);
props.setData_grabber(data.articles)
};
fetcher();
}, [props.btn_data]);
return (
<div>
<form className="d-flex">
<input
onChange={(e) => props.setInput_data(e.target.value)}
value={props.input_data}
className="form-control me-2"
type="search"
placeholder="Search"
aria-label="Search"
/>
<button
className="btn btn-outline-success"
type="submit"
onClick={onClicker}
>
Search
</button>
</form>
</div>
NewsItem.js
import React, { Component } from "react";
export default class NewsItem extends Component {
render() {
const {title, description, url, urlToImage} = this.props.data
const defaultImage = `https://blogger.googleusercontent.com/img/a/AVvXsEh20SgNNsDlKyWWmB7XgB5SfFY10M6CqJAq93HwGtssTn2cWz6w9zHPjXf91WwoWr27QeaC4HsGv2NxPOXUdvk6xodUojnw8rUuAkEMY3Qb4ucoVpN3nSyF8JW_xVDWa2aSMEWH387hPsfouSJyClLNburIcDbXIeJamuTHwiSvw4hdNnqeeICcvg1wrQ=w1200-h630-p-k-no-nu`
return (
<div>
<div className="card">
<img src={urlToImage?urlToImage:defaultImage} className="card-img-top" alt="..." />
<div className="card-body">
<h5 className="card-title">{title?title:'No title available'}</h5>
<p className="card-text">
{description?description.slice(0, 50):"no description available"}...
</p>
<a href={url} target="_blank" rel="noreferrer"className="btn btn-primary">
read more
</a>
</div>
</div>
</div>
);
}
}
One fix could be to
make a variable of the updated state:
in the UseEffect ,
add :
const updated = data.articles
props.setData_grabber(updated)
Check whether data_grabber array is empty or not and then do the rendering inside App component as follows.
{
data_grabber.length > 0 ? (
data_grabber.map((news_data) => {
return <NewsItem news_data={news_data} />;
})
) : (
<div>No data available</div>
);
}
{ data_grabber !== undefined && data_grabber.length > 0 ? data_grabber.map((news_data)=>{
return(
<NewsItem news_data={news_data}/>
)
}):<div>No data available</div>}
Check data_grabber is undefined or empty.
Then, fix NewsItem props.data like this.
export default class NewsItem extends Component {
render() {
const {title, description, url, urlToImage} = this.props.news_data
also fix here in useEffect
useEffect(() => {
const fetcher = async () => {
const link = `https://newsapi.org/v2/everything?q=${props.btn_data}&apiKey=c990aa0235da4635997afd1f7459860c`;
const raw_data = await fetch(link);
const data = await raw_data.json();
console.log(data);
if(data.articles){
props.setData_grabber(data.articles)
}
};
fetcher();
After submitting with UpdateParams, the new url is called and a JSON object with the new queried data is returned as expected.
The form updates the two state vars.
However, the products in the all-products view are not updated to reflect the form input.
What do I need to do to to refresh the render to reflect the new data in product?
//all-products.js
import Link from 'next/link'
import React from 'react';
import { useState } from 'react';
//gets data from local api
async function getData(rank, keyword){
const res = await fetch(`http://localhost:4000/api?top=${rank}&keyword=${keyword}`);
return res;
}
export async function getStaticProps() {
const rank = 5;
const keyword = "shorts";
const response = await getData(rank, keyword);
const products = await response.json();
console.log(products);
if (!products) {
return {
notFound: true,
}
}
return {
props: {
products,
},
}
}
export default function AllProducts(stuff) {
let {products} = stuff;
const [rank, setRank] = useState("3");
const [keyword, setKeyword] = useState("shoes");
//from form
const updateParams = async (e) => {
e.preventDefault();
const response= await getData(rank, keyword);
products = await response.json();
}
return (
<div>
<input
type='text'
placeholder='topRank'
value={rank}
onChange={e => setRank(e.target.value)}
/>
<input
type="text"
placeholder='searchTerm'
value={keyword}
onChange={e => setKeyword(e.target.value)}
/>
<button
type='submit'
onClick={updateParams}>
Update Params</button>
<ul>
{products.Products.map((product) => {
return (
<div key={product.Id}>
<li>{product.Name}</li>
<li><img width={300} src={ product.imgUrl } alt="product image" /></li>
</div>
) }
)}
</ul>
</div>
)
}
getStaticProps is run at build-time so it'll provide the data that's available at that time. To update the UI after the user interacts with the form you should put products into state and update it once new params are submitted and you retrieve the new products.
// all-products.js - removed irrelevant code for simplicity
export default function AllProducts(stuff) {
const [products, setProducts] = useState(stuff.products);
//...
const updateParams = async (e) => {
e.preventDefault();
const response = await getData(rank, keyword);
const newProducts = await response.json();
setProducts(newProducts);
}
return (
//...
<ul>
{products.Products.map((product) => {
return (
<div key={product.Id}>
<li>{product.Name}</li>
<li><img width={300} src={product.imgUrl} alt="product image" /></li>
</div>
)
})}
</ul>
//...
)
}
and thank you in advance for any help.
I am trying to build a web app that fetches data from an API, in this case a movie database API, but when i am trying to map all the movies from a specific title search i get the .map is not a function error, what i am doing wrong ? Can't i use useState to display the data ?
When i do console.log (search) i can see the array with all the data :
import React, {useEffect, useState} from 'react';
import axios from 'axios';
export default function RandomFacts() {
const [input, setInput] = useState('');
const [search, setSearch] = useState(['']);
useEffect(() => {
apiCall();
}, [input]);
const moviesList = search && search.map((movie, index) =>
<div className="movies" key="index">
<li><h2>{movie.Title}</h2></li>
<li><img src={movie.Poster} alt="poster" /></li>
</div>,
);
const apiCall = async () => {
const url = 'http://www.omdbapi.com/?s='+input+'&page=1&apikey=536a34c3';
try {
const response = await axios.get(url);
if (response.status === 200 && response !== undefined) {
const data = response.data;
setSearch(data.Search);
console.log(search);
}
} catch (error) {
console.log(error);
}
};
return (
<div className="main">
<h1>Movies</h1>
<div className="textInput">
<form>
<label>
<input type="text" value={input}
onChange={(e) => setInput(e.target.value)}
/>
</label>
</form>
</div>
<div className="movies">
{moviesList}
</div>
</div>
);
}
The API is returning a response Object with a data key containing the keys Search, TotalResults, and Response. You're trying to map this response Object instead of the Array contained in response.data.Search.
So you should be using setSearch(response.data.Search).