How to update a react component after a fetch - javascript

I am learning react.
I have a simple react app sample that :
Fetch users
Once users are fetched, show their name on a Card
What I'd like to do is to expand this sample. Instead of using a simple list of users, I'd like to use a list of pokemons. What I try to do is :
Fetch the list of pokemon and add in state.pokemons
Show the Card with the pokemon name from state.pokemons
From that list, get the URL to fetch the detail of the given pokemon and add in state.pokemonsDetails
From the state.pokemonsDetails, update the Cards list to show the image of the pokemon.
My problem is: I don't even know how to re-render the Cards list after a second fetch.
My question is: How to update the Cards list after the second fetch?
See my code below:
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox"
import Scroll from "../components/Scroll"
import './App.css';
class App extends React.Component{
constructor(){
super();
this.state = {
pokemons:[],
pokemonsDetails:[],
searchfield: ''
}
}
getPokemons = async function(){
const response = await fetch('https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20');
const data = await response.json();
this.setState({pokemons:data.results})
}
getPokemonDetails = async function(url){
//fetch function returns a Promise
const response = await fetch(url);
const data = await response.json();
//console.log('getPokemonDetails', data);
this.setState({pokemonsDetails:data});
}
componentDidMount(){
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value})
}
render(){
const {pokemons, pokemonsDetails, searchfield} = this.state;
if(pokemons.length === 0){
console.log('Loading...');
return <h1>Loading....</h1>
}else if (pokemonsDetails.length === 0){
console.log('Loading details...');
pokemons.map(pokemon => {
return this.getPokemonDetails(pokemon.url);
});
return <h1>Loading details....</h1>
}else{
return(
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange}/>
<Scroll>
<CardList pokemons={pokemons}/>
</Scroll>
</div>
);
}
}
}
export default App;
Some remarks :
I can see a problem where my Cards list is first created with state.pokemons, then, I would need to update Cards list with state.pokemonsDetails. The array is not the same.
Second problem, I don't even know how to call the render function after state.pokemonsDetails is filled with the fetch. I set the state, but it looks like render is not called every time
More a question than a remark. The way I update my state in getPokemonDetails might be incorrect. I keep only one detail for one given pokemon. How to keep a list of details? Should I use something else than setState to expand pokemonsDetails array?

You can combine 2 API calls before pokemons state update that would help you to control UI re-renderings better
You can try the below approach with some comments
Side note that I removed pokemonDetails state, so you won't see the loading elements for pokemonDetails as well
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox";
import Scroll from "../components/Scroll";
import "./App.css";
class App extends React.Component {
constructor() {
super();
this.state = {
pokemons: [],
searchfield: ""
};
}
getPokemons = async function () {
const response = await fetch(
"https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20"
);
const data = await response.json();
//try to get all pokemon details at once with fetched URLs
const pokemonDetails = await Promise.all(
data.results.map((result) => this.getPokemonDetails(result.url))
);
//map the first and second API response data by names
const mappedPokemons = pokemonDetails.map((pokemon) => {
const pokemonDetail = pokemonDetails.find(
(details) => details.name === pokemon.name
);
return { ...pokemon, ...pokemonDetail };
});
//use mapped pokemons for UI display
this.setState({ pokemons: mappedPokemons });
};
getPokemonDetails = async function (url) {
return fetch(url).then((response) => response.json());
};
componentDidMount() {
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({ searchfield: event.target.value });
};
render() {
const { pokemons, searchfield } = this.state;
if (pokemons.length === 0) {
return <h1>Loading....</h1>;
} else {
return (
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange} />
<Scroll>
<CardList pokemons={pokemons} />
</Scroll>
</div>
);
}
}
}
export default App;
Sandbox
If you want to update pokemon details gradually, you can try the below approach
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox";
import Scroll from "../components/Scroll";
import "./App.css";
class App extends React.Component {
constructor() {
super();
this.state = {
pokemons: [],
searchfield: ""
};
}
getPokemons = async function () {
const response = await fetch(
"https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20"
);
const data = await response.json();
this.setState({ pokemons: data.results });
for (const { url } of data.results) {
this.getPokemonDetails(url).then((pokemonDetails) => {
this.setState((prevState) => ({
pokemons: prevState.pokemons.map((pokemon) =>
pokemon.name === pokemonDetails.name
? { ...pokemon, ...pokemonDetails }
: pokemon
)
}));
});
}
};
getPokemonDetails = async function (url) {
return fetch(url).then((response) => response.json());
};
componentDidMount() {
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({ searchfield: event.target.value });
};
render() {
const { pokemons, searchfield } = this.state;
if (pokemons.length === 0) {
return <h1>Loading....</h1>;
} else {
return (
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange} />
<Scroll>
<CardList pokemons={pokemons} />
</Scroll>
</div>
);
}
}
}
export default App;
Sandbox
Side note that this approach may cause the performance issue because it will keep hitting API for fetching pokemon details multiple times and updating on the same state for UI re-rendering

Related

How to fetch data before render functionnal component in react js

Here Below my code I would like to retrieve all data before starting the render of my component, is there any way to do that in react ? I guess it's maybe a simple code line but as I'm new in coding I still don't know all react components behavior. Thanks for your answer.
import { useState, useEffect } from "react";
import axios from "axios";
import Cookies from "js-cookie";
// import material ui
import CircularProgress from "#mui/material/CircularProgress";
import Box from "#mui/material/Box";
// import config file
import { SERVER_URL } from "../../configEnv";
const Products = ({ catList }) => {
// catList is data coming from app.js file in format Array[objects...]
console.log("catList ==>", catList);
const [isLoading, setIsLoading] = useState(true);
const [dataSku, setDataSku] = useState([]);
console.log("datasku ==>", dataSku);
const tab = [];
useEffect(() => {
// Based on the catList tab I fetch additionnal data linked with each object of catList array
catList.slice(0, 2).forEach(async (element) => {
const { data } = await axios.post(`${SERVER_URL}/products`, {
product_skus: element.product_skus,
});
// The result I receive from the call is an array of objects that I push inside the Tab variable
tab.push({ name: element.name, content: data });
setDataSku(tab);
console.log("tab ==>", tab);
setIsLoading(false);
});
}, [catList]);
return isLoading ? (
<Box sx={{ display: "flex" }}>
{console.log("there")}
<CircularProgress />
</Box>
) : (
<div className="products-container">
<div>LEFT BAR</div>
<div>
{dataSku.map((elem) => {
return (
<div>
<h2>{elem.name}</h2>
</div>
);
})}
</div>
</div>
);
};
export default Products; ```
#Jessy use your loading state to fetch data once,
In your useEffect, check for loading,
useEffect(() => {
if(loading) {
catList.slice(0, 2).forEach(async (element) => {
const { data } = await axios.post(`${SERVER_URL}/products`, {
product_skus: element.product_skus,
});
tab.push({ name: element.name, content: data });
setDataSku(tab);
console.log("tab ==>", tab);
setIsLoading(false);
});
}
}, [catList]);`
I finally managed to displayed all results by adding this condition on the isLoading
if (tab.length === catList.length) {
setIsLoading(false);
}
Many thanks guys for your insight :)

Mounting Data into React State Returning Error

I have some data which I'm calling from an API. I was to update the data into my declared state which some of it works. But one of my state data is returning an error that "Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the component will unmount method."
Below is my Image Data file. The line giving the error is commented out in the return method.
import React, { Component } from "react";
import axios from "axios";
import { Card } from 'react-bootstrap';
export default class imagedata extends Component {
state = {
img: "",
infodata: [],
alldata: [],
};
componentDidMount() {
this.fetchCatDetails();
}
fetchCatDetails = async () => {
const info = `https://api.thecatapi.com/v1/breeds`;
const url = `https://api.thecatapi.com/v1/images/search?breed_id=${this.props.option.id}`;
try {
const response = await axios.get(url);
// console.log(response)
const img = await response.data[0].url;
// console.log(alldata)
this.setState({
img
});
const allresult = await axios.get(url);
const alldata = await allresult.data[0];
this.setState({
alldata,
});
const response1 = await axios.get(info);
const infodata = await response1.data;
// console.log(infodata)
this.setState({
infodata,
});
} catch (error) {
console.log(error);
}
};
render() {
const {
option: {
name,
origin,
temperament,
life_span,
weight: { metric },
description,
},
} = this.props;
return (
<div className="center">
<img src={this.state.img} alt="" loading="" className="img"/> <br/>
{name} <br/>
{origin} <br/>
{description}
{this.state.alldata.map((item, id) => (
<div key={id}>
{/* This line below is the line returning the error */}
{console.log(item)}
</div>
))}
</div>
);
}
}
Here is my index.js
import React, { Component } from "react";
import ReactDOM from "react-dom";
import axios from "axios";
import Imagedata from "./Imagedata";
import './style.css'
class App extends Component {
state = {
data: [],
};
componentDidMount() {
this.fetchData();
}
fetchData = async () => {
const url = "https://api.thecatapi.com/v1/breeds";
try {
const response = await axios.get(url);
const data = await response.data;
this.setState({
data,
});
} catch (error) {
console.log(error);
}
};
render() {
return (
<div className="App">
<h1>React Component Life Cycle</h1>
<h1>Calling API</h1>
<div>
{this.state.data.map((item, id) => (
<div key={id}>
<Imagedata option={item} />
</div>
))}
</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));

React app not showing data from api when setting state in componentDidMount

I have some react code where i'm trying to show a list of data in a table. This is the code. The console.log call in the getPagedData displays the correct results in the console but the render is not showing anything.
Seems to be a timing issue with asynchronous call but im stumped. Any ideas why?
import React, { Component } from 'react';
import { getSearchResults} from '../services/searchService';
import AircraftsTable from '../components/aircraftsTable';
import Pagination from '../components/common/pagination';
import qs from 'qs';
class SearchResults extends Component {
state = {
aircrafts: [],
totalCount:0,
currentPage: 1,
pageSize: 10
}
componentDidMount(){
const { aircrafts, totalCount} = this.getPagedData();
this.setState(aircrafts, totalCount);
}
handlePageChange = page => {
this.setState({ currentPage: page });
};
getPagedData = async () => {
const queryString = this.getQueryString();
const searchResults = await getSearchResults(queryString);
const aircrafts = searchResults.data.aircrafts;
const totalCount = searchResults.data.totalCount;
console.log(aircrafts, totalCount);
return [ aircrafts, totalCount ];
}
getQueryString(){
let criteria = this.props.history.location.state?.data;
criteria.currentPage = this.state.currentPage;
criteria.pageSize = this.state.pageSize;
const criteriaString = qs.stringify(criteria);
return criteriaString;
}
render() {
const { aircrafts, totalCount, pageSize, currentPage} = this.state;
return (
<React.Fragment>
<AircraftsTable
aircrafts={aircrafts}
/>
<Pagination
itemsCount={totalCount}
pageSize={pageSize}
currentPage={currentPage}
onPageChange={this.handlePageChange}
/>
</React.Fragment>
);
}
}
export default SearchResults;
Since you're returning an array and not an object from getPagedData you need to update your componentDidMount to:
componentDidMount(){
const [aircrafts, totalCount] = this.getPagedData();
this.setState({ aircrafts, totalCount });
}
In getPageData function you are returning an array. But you are destructuring an object
return [ aircrafts, totalCount ];
instead of this
return { aircrafts, totalCount };

Persisting log-in with React?

so, I'm currently working on a MERN app that successfully saves JWT tokens via library localstorage, surviving any refresh attempts (new users show up in the database, etc, the backend is all working as intended).
The issue is, the frontend React app has 'user' set to 'null' by default in the container's state, so that incongruency is what keeps logging users upon out upon re-rendering despite the JWT. I've been stuck on this for over a day now, have tried implementing a variety of possible solutions, have received help from my instructors, etc, nothing is achieving the desired result- does anyone have any advice?
I have attached the code from my container for reference (excuse the messiness, I'm in the middle of being too frustrated with this whole thing to do much about that), Furthermore I also got a bunch of other components and files that interact with my container in some way or other, won't attach them now but if anyone feels that the extra context is needed in order to help then I will do so. Thank you!
import React, { Component } from "react";
import { getItems } from "../services/items";
import Routes from "../routes";
import Header from "../screens/Header";
import { verifyToken } from '../services/auth'
export default class Container extends Component {
constructor(props) {
super(props);
this.state = {
user: null,
items: [],
isLoggedIn: false
};
}
async componentDidMount() {
// const user = await verifyToken();
// if (user) {
try {
const items = await getItems();
this.setState({
items,
isLoggedIn: true
});
}
catch (err) {
console.error(err);
}
}
addItem = item =>
this.setState({
items: [item, ...this.state.items]
});
editItem = (itemId, item) => {
const updateIndex = this.state.items.findIndex(
element => element._id === itemId
),
items = [...this.state.items];
items[updateIndex] = item;
this.setState({
items
});
};
destroyItem = item => {
const destroyIndex = this.state.items.findIndex(
element => element._id === item._id
),
items = [...this.state.items];
if (destroyIndex > -1) {
items.splice(destroyIndex, 1);
this.setState({
items
});
}
};
setUser = user => this.setState({ user });
//verifyUser = user => (localStorage.getItem('token')) ? this.setState({ user, isLoggedIn: true }) : null
clearUser = () => this.setState({ user: null });
render() {
// const token = localStorage.getItem('token');
// console.log(token)
const { user, items } = this.state;
return (
<div className="container-landing">
<Header user={user} />
<main className="container">
<Routes
items={items}
user={user}
setUser={this.setUser}
addItem={this.addItem}
editItem={this.editItem}
destroyItem={this.destroyItem}
clearUser={this.clearUser}
//verifyUser={this.verifyUser}
/>
</main>
</div>
);
}
}

onSubmit is not executing async function

I'm trying to submit a function that will generate a gif, when pressing the get gif button.
However, it does not show anything in the console, and the page reloads.
1) I want the client to type in a value
2) set the value to the like so
ex.
http://api.giphy.com/v1/gifs/search?q=USER_VALUE&api_key=iBXhsCDYcnktw8n3WSJvIUQCXRqVv8AP&limit=5
3) fetch the value and return like the following
Current project
https://stackblitz.com/edit/react-4mzteg?file=index.js
App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import Card from './Card';
import { throws } from 'assert';
class App extends Component {
constructor(props){
super(props);
this.state = {
query: '',
slug:undefined,
url:undefined
}
this.onChange = this.onChange.bind(this);
}
onChange(e){
this.setState({
query: e.target.query
})
}
getGIY = async (e) =>{
try {
const {slug, url} = this.state;
const query = this.state._query
const response = await fetch(`http://api.giphy.com/v1/gifs/search?q=${query}&api_key=iBXhsCDYcnktw8n3WSJvIUQCXRqVv8AP&limit=5`);
const data = await response.json();
const mainData = data.data;
if(query){
this.setState({
slug: mainData[0].title,
url: mainData[0].images.downsized.url
});
console.log(mainData);
}
} catch (error) {
console.log(error);
}
}
render() {
return(
<div>
<h1> Welcome</h1>
<form onSubmit={this.props.getGIY}>
<input type="text" name="query" onChange={this.onChange} ref={(input) => {this.state._query = input}} placeholder="Search GIF..."/>
<button>Get GIF</button>
</form>
<Card slug={this.state.slug} url={this.state.url}/>
</div>
);
}
}
export default App;
Card.js
import React, {Component} from 'react';
const Styles = {
width: '300px',
height: '300px'
}
class Card extends React.Component {
render() {
return (
<div>
<h1>{this.props.slug}</h1>
<div>
<img src={this.props.url}/>
</div>
</div>
);
}
}
export default Card;
You are missing 2 3 4 things
1) instead of this.props.getGIY you need to use this.getGIY
2) as you are using form you need to preventdefault using
getGIY = async (e) =>{
e.preventDefault();
3) instead of e.target.query you need to get e.target.value
4) instead of const query = this.state._query you need to use const query = this.state.query your state name is query
onChange(e){
this.setState({
query: e.target.value
})
}
Demo
Your getGIY function
getGIY = async (e) =>{
e.preventDefault();
try {
const {slug, url} = this.state;
const query = this.state._query
const response = await fetch(`http://api.giphy.com/v1/gifs/search?q=${query}&api_key=iBXhsCDYcnktw8n3WSJvIUQCXRqVv8AP&limit=5`);
const data = await response.json();
const mainData = data.data;
if(query){
this.setState({
slug: mainData[0].title,
url: mainData[0].images.downsized.url
});
console.log(mainData);
}
} catch (error) {
console.log(error);
}
}
Your form
<form onSubmit={this.getGIY}>
<input type="text" name="query" onChange={this.onChange} ref={(input) => {this.state._query = input}} placeholder="Search GIF..."/>
<button>Get GIF</button>
</form>
Mixing promises and try/catch blocks is a little messy as promises themselves duplicate much of the behavior of try/catch blocks. Promises are also chainable. I suggest this edit for your getGIY function. It's just as readable as the existing try/catch w/ unchained promises but more idiomatic (e.g. if THIS is successful, THEN do this next), and more importantly it's a bit more succinct.
getGIY = async (e) =>{
e.preventDefault();
const { query } = this.state;
/* fetch and response.json return promises */
await fetch(`http://api.giphy.com/v1/gifs/search?q=${query}&api_key=iBXhsCDYcnktw8n3WSJvIUQCXRqVv8AP&limit=5`)
// fetch resolved with valid response
.then(response => response.json())
// response.json() resolved with valid JSON data
// ({ data }) is object destructuring (i.e. data.data)
.then(({ data }) => {
this.setState({
slug: data[0].title,
url: data[0].images.downsized.url
});
})
/* use catch block to catch any errors or rejected promises */
.catch(console.log); // any errors sent to log
}

Categories