What is the proper way to fetch JSON in React with Hooks? - javascript

So I have this application that displays random quotes that are pulled as JSON data from an API. It's my first foray into React so it is not really well done. Initially, I had all of my code stored in one component - but this was obviously not best practices because I had multiple things that could be split into components, i.e. a quote, a footer, a share button.
The issue I ran into when I split it up is that I didn't know how to share state between component files (for sharing to Twitter or other additional features) because I fetch the data like this:
/* this function accesses the API and returns a json */
export default function fetchQuote() {
return fetch('https://programming-quotes-api.herokuapp.com/quotes/random') // fetch a response from the api
.then((response) => {
let json = response.json(); // then assign the JSON'd response to a var
return json; // return that bad boy
});
}
which originally was called within the component class like so:
/* component for the quotes */
export default class Quote extends React.Component {
/* placeholder */
constructor(props) {
super(props);
this.state = {
quoteAuthor: "Rick Osborne",
quote: "Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live."
}
}
/* actually render things */
render() {
return (
<div className="quotes">
<h1>{this.state.quoteAuthor}</h1>
<p>{this.state.quote}</p>
<div className="button">
<button id="button" onClick={this.update}>New quote</button>
</div>
</div>
);
}
/* async fetch the quotes and reassign the variables to them once processed */
update = async() => {
let response = await fetchQuote();
console.log(response);
this.setState({
quoteAuthor: response.author,
quote: response.en
});
};
}
From my understanding, React's hooks seemed to solve my problem because I could use useState and useEffect which I tried to implement as follows (with the original fetchQuote() function untouched):
export default function Quote() {
const [author, setAuthor] = useState("Rick Osborne");
const [quote, setQuote] = useState(
"Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live."
);
let json = fetchQuote();
useEffect (() => {
setAuthor(json.author);
setQuote(json.quote);
console.log(json);
});
return (
<div className="quotes">
<h1>{author}</h1>
<p>{quote}</p>
<div className="button">
<button id="button" onClick={async () => json = await fetchQuote()}>
New quote
</button>
</div>
</div>
)
}
However, no errors are thrown except the area in which the quote is displayed shows empty and calling console.log(json) within the useEffect simply returns
Promise { <state>: "pending" }
Promise { <state>: "pending" }
Am I using Hooks properly? How can I properly update the state with the JSON data?

It looks like the promise from fetch isn't resolving.
Try this:
export default Quote = () => {
const [author, setAuthor] = useState("Rick Osborne");
const [quote, setQuote] = useState('');
const fetchMyAPI = async () => {
let json = await fetchQuote();
setAuthor(json.author);
setQuote(json.quote);
}
useEffect(() => {
fetchMyAPI();
}, []);
return (
<div className="quotes">
<h1>{author}</h1>
<p>{quote}</p>
<div className="button">
<button id="button" onClick={fetchMyAPI}>
New quote
</button>
</div>
</div>
)
This called fetchMyAPI onMount, and calls it whenever you click on New Quote.

Related

After refreshing, React props turn undefined

I'm fetching from an API where I want to pass the response object as a prop onto a child component from App.js and use it in my Tags.js file. However, it only works after one time and when I refresh it, it gives me an error saying the props.response.names.length is undefined. I tried using the useEffect function to try and update it but it didn't work. I would appreciate any help.
My App.js file (still some remnants of when you run "npx create-react-app my-app"):
import './App.css';
import Tags from './Tags.js';
import React, { useState, useEffect } from 'react';
function App() {
const makeRequest = async () => {
try {
let response = await fetch('RANDOM_API_URL');
let json = await response.json();
setResponse(json);
} catch (error) {
console.log(error);
}
}
const [response, setResponse] = useState(makeRequest);
useEffect(() => {
setResponse(makeRequest);
}, []);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
<Tags response={response}></Tags>
</div>
);
}
export default App;
My Tags.js:
import './App.js';
function Tags(props) {
const makeButtons = () => {
let result = [];
for (let i = 0; i < props.response.names.length; i++) {
result.push(<button key={i}>hello</button>);
}
return result;
}
return (
<div>
{makeButtons()}
</div>
);
}
export default Tags;
Your makeRequest function is async, and sets state internally. The reason you get this bug is that you don't always have a response.names to read length from - you only have an empty response object.
Either make sure you always have the names array available in state, or avoid rendering your Tags component when names is not present in state.
Also, try to avoid being creative with your dependency array, it's there for a reason. I see why you didn't include makeRequest in it though, since you create a new function on every render. That's something to keep in mind when you transition from class components to functional components. Class methods are stable across renders, functions declared in a functional component are not. To mimic a class method, you can declare functions using the useCallback hook, but again you need to include the dependency array. In your case, you can just create the async function inside useEffect, and then call it.
const [response, setResponse] = useState({names: []});
// or const [response, setResponse] = useState();
// and then, in Tags:
// function Tags({response = {names: []}) {
useEffect(() => {
const makeRequest = async () => {
try {
let response = await fetch('RANDOM_API_URL');
let json = await response.json();
setResponse(json);
} catch (error) {
console.log(error);
}
}
makeRequest();
}, []);
Supplying a default response prop to Tags will make sure you can read the length of names even before you have a response.
Looking more closely on your Tags component, I think it should be something like this:
// don't import App.js here
export default function Tags({response = {names: []}){
return (
<div>
{response.names.map(name => {
return <button key={name}>Hello {name}</button>
})}
</div>
)
}
Don't use index as key, that will cause problems if you rearrange the names in your array. And I guess you want to supply an onClick function to your buttons.

Why does my React app not re-render or display changes on the DOM except I relaod?

import { useEffect, useState } from "react";
function Popular() {
const [popular, setPopular] = useState([]);
useEffect(() => {
getPopular();
}, []);
const getPopular = async () => {
const api = await fetch(
`https://api.spoonacular.com/recipes/random?apiKey=${process.env.REACT_APP_RECIPE_API_KEY}&number=9`
);
const data = await api.json();
setPopular(data.recipes);
};
return (
<div>
{popular.map((recipe) => {
return (
<div>
<p>{recipe.title}</p>
</div>
);
})}
</div>
);
}
export default Popular;
I am pretty new to React, and I encountered this issue which I have been trying to fix to no avail. The code is a component that is to return a list of recipe title to my app. I am fetching data from an API in the getPopular() function which is set to the setPopular function variable of the useState() method. But when I save my work and return to the browser, the changes does not display. The list does not display, but if I console.log(data.recipes) it displays on the console.
Before now, if I made any change (maybe a text change) the React app renders it without reloading, but now I have to reload the page before I see the change.
Please how do I fix this issue? So that I can see changes without having to reload the page manually.
Not saying that this is the problem, but getPopular() should not be called after its declaration? By this I mean:
const getPopular = async () => {
const api = await fetch(
/...
};
useEffect(() => {
getPopular();
}, []);
Another thing that bugs me is, although JS/React is case sensitive, I really think you should avoid having a const called popular, since your functions is Popular.
Please, let me know if the order did matter for your problem. I will review some react classes soon, if i get another inside, i'll let you know.

ReactJS :: Jest Testing :: "TypeError: Cannot read property 'then' of undefined"

I am currently having some trouble compiling a test for an online study task to see whether the fetch() function of my weather application is working correctly.
I have made use of the useEffect() hook to fetch the data from the OpenWeather API to store and render once the API's URL changes.
I am new to Jest testing and have tried a couple of things, following tutorials and other sources, but am unfortunately not having any success. My current solution is returning the following error: "TypeError: Cannot read property 'then' of undefined"
Please see below my code:
App.js
// Imported hooks and react libraries.
import React, { useState, useEffect } from 'react';
// Imported stylesheet.
import './App.css';
// Imported components.
import Header from './components/Header';
import Footer from './components/Footer';
// Imported countries from i18n-iso-countries to get the iso code and return the country name in English.
import countries from 'i18n-iso-countries';
// Imported icons from Font Awesome.
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import {
faCloudSunRain,
faHandHoldingWater,
faHandSparkles,
faMapMarkerAlt,
faSearchLocation,
faTemperatureHigh,
faTemperatureLow,
faWind
} from '#fortawesome/free-solid-svg-icons';
countries.registerLocale(require('i18n-iso-countries/langs/en.json'));
function App() {
// Setting the initial states of the app to store the response and the locations. Using the useState hook to set the data. Showing Durban as
// an example.
const [apiData, setApiData] = useState({});
const [getState, setGetState] = useState('Durban');
const [state, setState] = useState('Durban');
// Constructing the API URL and accessing the key via the process.env variable.
const apiKey = process.env.REACT_APP_API_KEY;
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?q=${state}&APPID=${apiKey}`;
console.log (process.env.REACT_APP_API_KEY);
// Using the useEffect hook to fetch the data from the API to store and render once the API's URL changes.
useEffect(() => {
fetch(apiUrl)
.then((res) => res.json())
.then((data) => setApiData(data));
}, [apiUrl]);
// Constructed an input handler to get the data once requested and to store in the getState.
const inputHandler = (event) => {
setGetState(event.target.value);
};
// Constructed a submit handler to handle the request once the search button is clicked.
const submitHandler = () => {
setState(getState);
};
// Constructed a kelvin to celsius converter to output the temperature in celsius.
const kelvinToCelsius = (k) => {
return (k - 273.15).toFixed(2);
};
// Constructed a miles to kilometers converter to output the temperature in kilometers.
const milesToKilometers = (k) => {
return (k * 3.6).toFixed(2);
};
// Created a function to capitalize the first letters of each part of the countries' names.
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
};
// Returning the data. Included the React Bootstrap stylesheet's link and called the "Header" and "Footer" components below. I also called the
// following from the API:
// {apiData.weather[0].icon} - The icon displaying the current conditions.
// {apiData.name} - The city's name.
// {countries.getName(apiData.sys.country, 'en', { select: 'official', })} - The country's name with the first letters capitalized.
// {kelvinToCelsius(apiData.main.temp_min)} - The minimum temperature.
// {kelvinToCelsius(apiData.main.temp_max)} - The maximum temperature.
// {kelvinToCelsius(apiData.main.feels_like)} - The "feels like" temperature, taking into account the temperatures and conditions.
// {apiData.weather[0].main} - The summarized condition.
// {capitalizeFirstLetter(apiData.weather[0].description)} - The full condition's description.
// {apiData.main.humidity} - The humidity percentage.
// {milesToKilometers(apiData.wind.speed)} - The wind speed.
// Called the inputHandler (input section) and submitHandler (button) to get the current state's values and added Font Awesome icons. Also
// added a loading message for if the page load takes a while. Currently only shows if there is no input or upon refresh.
return (
<div className="App">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css"></link>
<Header />
<div className="container">
<div className="searchsection">
<label htmlFor="location-name">Enter Location:</label>
<input
type="text"
id="location-name"
onChange={inputHandler}
value={getState}
/>
<button onClick={submitHandler}><FontAwesomeIcon icon={faSearchLocation} /></button>
</div>
<div className="mt-3 mx-auto" style={{ width: '60vw' }}>
{apiData.main ? (
<div id="weathercontainer">
<div id="mainweather">
<img
src={`http://openweathermap.org/img/wn/${apiData.weather[0].icon}#2x.png`}
alt="weather status icon"
className="weather-icon"
/>
<p className="h2">{kelvinToCelsius(apiData.main.temp)}°C</p>
<h3><FontAwesomeIcon icon={faMapMarkerAlt} /> {apiData.name}</h3>
<h3>{countries.getName(apiData.sys.country, 'en', { select: 'official', })}</h3>
</div>
<div className="temperatureconditions">
<div id="temperature">
<h5>Temperature:</h5>
<p><FontAwesomeIcon icon={faTemperatureLow} /> {kelvinToCelsius(apiData.main.temp_min)}°C</p>
<p><FontAwesomeIcon icon={faTemperatureHigh} /> {kelvinToCelsius(apiData.main.temp_max)}°C</p>
<p><FontAwesomeIcon icon={faHandSparkles} /> Feels like: {kelvinToCelsius(apiData.main.feels_like)}°C</p>
</div>
<div id="conditions">
<h5>Conditions:</h5>
<p><FontAwesomeIcon icon={faCloudSunRain} /> {apiData.weather[0].main}: {capitalizeFirstLetter(apiData.weather[0].description)}</p>
<p><FontAwesomeIcon icon={faHandHoldingWater} /> Humidity: {apiData.main.humidity}%</p>
<p><FontAwesomeIcon icon={faWind} /> Wind Speed: {milesToKilometers(apiData.wind.speed)} km/h</p>
</div>
</div>
</div>
) : (
<h1 id="loading">Weather Bot is Loading...</h1>
)}
</div>
</div>
<Footer />
</div>
);
}
// Exported App to Index.js.
export default App;
App.Fetch.React.test.js
import React from 'react';
import App from '../App';
import { render, screen, act } from '#testing-library/react';
global.fetch = jest.fn(() =>
Promise.resolve({
json: () =>
Promise.resolve({
value: "Durban"
}),
})
);
describe("App", () => {
it("loads Durban city name", async () => {
await act(async () => render(<App />));
expect(screen.getByText("Durban")).toBeInTheDocument();
});
});
Does anyone mind helping?
I have managed to find a solution to run the test successfully.
The test's code is as follows (with notes for referencing):
/* Storing a reference to the global.fetch function so that we can use it to cleanup the mock after we're done testing. */
const unmockedFetch = global.fetch
/* Fetching the Promise with the JSON method, which also returns the Promise with the data. */
beforeAll(() => {
global.fetch = () =>
Promise.resolve({
json: () => Promise.resolve([]),
})
})
/* Using the afterAll() jest hook and calling the global.fetch function to cleanup mock test. */
afterAll(() => {
global.fetch = unmockedFetch
})
/* Adding a description of what should be executed and describing the test that will determine whether it is executed successfully or not.
Utilizing the async function due to the await keyword being used to invoke asynchronous code. Using the expect() and toHaveProperty() functions
to see whether the fetched data from JSON matches the keys stipulated. */
describe('Displaying the temperature and the wind speed', () => {
test('Is it working?', async () => {
const json = await withFetch()
expect(json).toHaveProperty(['main', 'temp']);
expect(json).toHaveProperty(['wind', 'speed']);
})
})
I hope that all is in order and that it will help someone else in future.

Display data from an API when clicking a button. Using ReactJS

I am trying to display the data of each character when I click the Info Button.
I know it is because in the onButtonClickHandler function it can not see the state. I have also tried this.state.person but it gives me an error saying "can not read state". And if I try just state.person it will give me "undefined".
What is the best way to do that? Thank you
API Link: https://swapi.dev/people/
import React from "react";
export default class FetchActors extends React.Component {
state = {
loading: true,
person: null
};
async componentDidMount() {
const url = "https://swapi.dev/api/people/";
const response = await fetch(url);
const data = await response.json();
this.setState({ person: data.results, loading: false });
}
render() {
if (this.state.loading) {
return <div>loading...</div>;
}
if (!this.state.person.length) {
return <div>didn't get a person</div>;
}
function onButtonClickHandler(state) {
console.log(state.person);
};
return (
<div>
<h1>Actors</h1>
{this.state.person.map(person =>(
<div>
<div>
{person.name}
<button onClick={onButtonClickHandler}>Info</button>
</div>
</div>
))}
<button onClick={onButtonClickHandler}>Enter</button>
</div>
);
}
}
Please correct me if I'm wrong
The most likely reason why you are seeing this is because of the way javascript internally works. The syntax:
function xyz() {
}
has an implicit this
Maybe try changing your code from:
function onButtonClickHandler(state) {
console.log(state.person);
};
to:
const onButtonClickHandler = () => {
console.log(this.state.person);
};
Further Reading: Here
You have defined your function onButtonClickHandler as a function that takes one argument, and logs the person property of that argument. The argument state in your function has nothing to do with the state of your component. As javascript sees it, they are two totally unrelated variables which just happen to have the same name.
function onButtonClickHandler(state) {
console.log(state.person);
};
When button calls onClick, it passes the event as the argument. So your onButtonClickHandler is logging the person property of the event, which obviously doesn't exist.
Since you are not using any information from the event, your function should take no arguments. As others have said, you should also move this function outside of the render() method so that it is not recreated on each render. The suggestion to use bind is not necessary if you use an arrow function, since these bind automatically.
export default class FetchActors extends React.Component {
/*...*/
onButtonClickHandler = () => {
console.log(this.state.person);
};
}
Inside render()
<button onClick={this.onButtonClickHandler}>Enter</button>
You could also define the function inline, as an arrow function which takes no arguments:
<button onClick={() => console.log(this.state.person)}>Enter</button>
If you are new to react, I recommend learning with function components rather than class components.
Edit:
Updating this answer regarding our comments. I was so caught up in explaining the errors from doing the wrong thing that I neglected to explain how to do the right thing!
I am trying to display the data of each character when I click the Info Button.
Once we call the API, we already have the info loaded for each character. We just need to know which one we want to display. You can add a property expanded to your state and use it to store the index (or id or name, whatever you want really) of the currently expanded item.
When we loop through to show the name and info button, we check if that character is the expanded one. If so, we show the character info.
Now the onClick handler of our button is responsible for setting state.expanded to the character that we clicked it from.
{this.state.person.map((person, i) =>(
<div>
<div>
{person.name}
<button onClick={() => this.setState({expanded: i})}>Info</button>
{this.state.expanded === i && (
<CharacterInfo
key={person.name}
person={person}
/>
)}
</div>
CodeSandbox Link
there are a few ways you can resolve your issue; I'll give you the more common approach.
You want to define your click handler as a class (instance) method, rather than declare it as a function inside the render method (you can define it as a function inside the render method, but that's probably not the best way to do it for a variety of reasons that are out of scope).
You will also have to bind it's 'this' value to the class (instance) because click handlers are triggered asynchronously.
Finally, add a button and trigger the fetch on click:
class Actors extends React.Component {
state = {
loading: false,
actors: undefined,
};
constructor(props) {
super(props);
this.fetchActors = this.fetchActors.bind(this);
}
async fetchActors() {
this.setState({ loading: true });
const url = "https://swapi.dev/api/people/";
const response = await fetch(url);
const data = await response.json();
this.setState({ actors: data.results, loading: false });
}
render() {
console.log('Actors: ', this.state.actors);
return <button onClick={this.fetchActors}>fetch actors</button>;
}
}
Sometimes i takes react a min to load the updated state.
import React from "react";
export default class FetchActors extends React.Component {
state = {
loading: true,
person: null
};
async componentDidMount() {
const url = "https://swapi.dev/api/people/";
const response = await fetch(url);
const data = await response.json();
if(!data.results) { // throw error }
this.setState({ person: data.results, loading: false }, () => {
console.log(this.state.person) // log out your data to verify
});
}
render() {
if (this.state.loading || !this.state.person) { // wait for person data
return <div>loading...</div>;
}else{
function onButtonClickHandler(state) { // just make a componentDidUpdate function
console.log(state.person);
};
return (
<div>
<h1>Actors</h1>
{this.state.person.map(person =>(
<div>
<div>
{person.name}
<button onClick={onButtonClickHandler}>Info</button>
</div>
</div>
))}
<button onClick={onButtonClickHandler}>Enter</button>
</div>
);
}
}}

TypeError: Cannot read property 'url' of undefined for JSONPlaceholder

I am trying to load pictures from jsonPlaceholder. When I console.log out the images array, it works successfully. The moment I try to call the URL from the element, it throws a TypeError. I tried to do it the asynchronous way, even this doesn't work. Is there any workaround for this? What possibly is the problem?
import React, {Component} from 'react';
import axios from 'axios';
class Home extends Component{
state = {
posts : [],
images : []
};
componentDidMount = async () => {
const res = await axios.get('https://jsonplaceholder.typicode.com/posts');
this.setState({posts: res.data.slice(0,10)})
const res2 = await axios.get('https://jsonplaceholder.typicode.com/photos');
this.setState({images: res2.data.slice(0,10)})
}
render = () => {
const {posts} = this.state.posts;
const {images} = this.state.images;
const PostList = this.state.posts.length ? (
this.state.posts.map((post,index) => {
return(
<div className = "post card" key = {post.id}>
<div className = "card-content">
<div className = "card-title">
{post.title}
</div>
<div className>
{(this.state.images[index].url)} //console.log(this.state.images[index] works, but .URL doesn't
</div>
<div>
<p>{post.body}</p>
</div>
</div>
</div>
)
})) : (<div className = "center">No Posts!</div> )
return(
<div className = "container">
<div className = "center">
{PostList}
</div>
</div>)
}
}
export default Home;
The issue was in multiple setState. Once you setState, it updates and renders asynchronously. So didn't wait for next setState. Better set both data in one call.
componentDidMount = async () => {
const res = await axios.get('https://jsonplaceholder.typicode.com/posts');
this.setState({posts: res.data.slice(0,10)})
const res2 = await axios.get('https://jsonplaceholder.typicode.com/photos');
this.setState({images: res2.data.slice(0,10)})
}
You can clean up code like this:
Working post: https://codesandbox.io/s/articlepodcast-wd2rv?file=/home.js:0-1214
import React, { Component } from "react";
import axios from "axios";
class Home extends Component {
constructor(props) {
super(props);
this.state = {
posts: [],
images: []
};
}
componentDidMount = async () => {
const res = await axios.get("https://jsonplaceholder.typicode.com/posts");
const res2 = await axios.get("https://jsonplaceholder.typicode.com/photos");
this.setState({
posts: res.data.slice(0, 10),
images: res2.data.slice(0, 10)
});
};
render = () => {
const { posts, images } = this.state;
const PostList = posts.length ? (
posts.map((post, index) => {
return (
<div className="post card" key={post.id}>
<div className="card-content">
<div className="card-title">{post.title}</div>
<div className>{images[index].url} </div>
<div>
<p>{post.body}</p>
</div>
</div>
</div>
);
})
) : (
<div className="center">No Posts!</div>
);
return (
<div className="container">
<div className="center">{PostList}</div>
</div>
);
};
}
export default Home;
Because of your first setState, it goes to render before second async-await.
const res = await axios.get('https://jsonplaceholder.typicode.com/posts');
const res2 = await axios.get('https://jsonplaceholder.typicode.com/photos');
this.setState({images: res2.data.slice(0,10), posts: res.data.slice(0,10)})
try like this.
better to have something like this;
this.setState({
posts: res.data.slice(0,10).map((post, index) => ({
...post,
image: res2.data.slice(0,10)[index]
}))
})
The Problem
The error happens because of the way the state is set in componentDidMount function. To break down why this happens I will explain what the code is trying to do:
First, Axios will send a get request to obtain posts, because this is an async action that is being awaited javascript will work on something else while the data is being obtained. So it will continue and run the render function the first time. The render function will return No Posts! as this.state.posts.length ? is false.
Then when we finish getting the posts the componentDidMount will continue, in this case it will update the state this.setState({posts: res.data.slice(0,10)}), every time setState is called it will tell react to run the render function for that component again to make sure the page reflects what is in our state. Then we get to the next line where we await to get the photos, while we are waiting render function will run a second-time because (as just said) we changed the sate of the component. At this point in time, we still don't have the photos as we are currently awaiting for them but, we do have posts, so the check to see if we have the posts this.state.posts.length ? will be true. Then when we get to images[index] it will be undefined because we have not finished obtaining the photos and setting them into the state yet. (I hope this helps you understand the problem).
The Solution
Update the state after we have obtained all data so that the second time the render function runs we have images:
const res = await axios.get("https://jsonplaceholder.typicode.com/posts");
const res2 = await axios.get("https://jsonplaceholder.typicode.com/photos");
this.setState({
posts: res.data.slice(0, 10),
images: res2.data.slice(0, 10)
});
I would also suggest adding a safety check to this.state.images like you did with posts to make sure you actually have the data before you try to use it. This is good practice to do for all data that comes from network calls because many unexpected things could happen when trying to obtain data from a service.
Lastly, I also suggest you try following through your code with the chrome debugger. This will help you follow what the code is doing to get a better understanding of why an issue like this would be happening. Here is a nice guide on how to do that.
https://developers.google.com/web/tools/chrome-devtools/javascript

Categories