I am trying to to an asynchronous fetch call once my modal is opened. I need that call because It fetched images and it will take around 5 seconds for the fetch to get a response.
So the modal should show first with the data and then once the fetch is complete, it should show the fetch data also.
My problem is that at the moment when calling the function with this () => {this.fetchImages(id) it is not called. I assume it's because the function is being assigned and not called.
But when I call the function fetchImages() without the () =>, I get this error :
Invariant Violation: Minified React error #31
This is my code, irrelevant part has been removed for simplicity:
renderModal = () => {
...
return (
<Modal open={openModal != null} onClose={this.closeModal}
little showCloseIcon={true} styles={modalStyles}>
<div className="item">
...
{() => {this.fetchImages(id)
.then(r => console.log("Fetch result" + r))}}
</div>
</Modal>
);
}
fetchImages = async (id) =>{
console.log("Request started")
try{
let myImages = null;
if(typeof(id) !== 'undefined' && id != null) {
myImages = await fetchImages(id);
console.log("Images: " + myImages);
return (
<div>
{Array.isArray(myImages) && myImages.length !== 0 && myImages.map((item, key) =>
<p>Image name: {item.name}, En device name: {item.name.en_US}</p>
)}
</div>
);
}
} catch (e) {
console.log("Failed")
}
}
EDIT
By changing the code as suggested by jack.benson and GalAbra, I ran into an issue where I am stuck in an endless loop. I will add the new code:
Once the page loads up the renderModal is called in render() method:
{this.renderModal()}
Then I have a button that would show the modal since modal contains a line :
<Modal open={openModal != null} onClose={this.closeModal}
little showCloseIcon={true} styles={modalStyles}>
It is called from here:
myMethod = (task) => {
...
return (
<div {...attrs}>
...
<button onClick={() => {this.showModal(documents[0])}}>{translate('show_more')} ยป</button>
</div>
}
</div>
</div>
);};
And the part to show the modal:
showModal = (document) => {
this.setState({ modalOpen: document });
};
And now the new renderModal():
renderModal = () => {
const doThing = async () => {
try {
const newImages = await downloadDeviceImages(id);
return { data: newImages };
} catch (e) {
console.log("Failed")
}
};
const test = async (id) => {
const imageData = await doThing(id);
console.log("after", imageData.data);
this.setState({
imageData: imageData.data
});
};
if(typeof(id) !== 'undefined' && id != null) {test(id);}
return (
<Modal open={openModal != null} onClose={this.closeModal}
little showCloseIcon={true} styles={modalStyles}>
<div className="item">
...
<div>
{this.state.imageData.map((item, key) =>
<p>Device name: {item.name}, En device name: {item.name.en_US}</p>
)}
</div>
</Modal>
);
}
The main part here is that the button will be clicked multiple times and it might have different ID on every click so a new request should be made every time.
So, this.fetchImages(id) returns a Promise, which React has no idea how to render. That is why you get the error. When you wrap it in () => { ... } you are actually creating a function that wraps your promise, but you are never calling the function. That is why it is not called. It is like saying:
function aThingToDo() {
console.log('Did thing!')
}
function doThing() {
aThingToDo();
}
Notice that I declared to functions, and neither of them were called. You need to call the function by explicitly adding parentheses after it (like doThing()).
That explains your error and the fact that your function is not called. Now, how to handle it. You want to wrap your fetching in a useEffect (as async stuff during render should be done). Then, if you want the data during render you can set state once it completes which will trigger a re-render. I put together a quick example of what I am talking about:
import React, { useEffect, useState } from "react";
import "./styles.css";
const doThing = async () => {
console.log("doing a thing");
return { data: "My data" };
};
export default function App() {
const [data, setData] = useState("");
useEffect(() => {
const test = async () => {
const data = await doThing();
setData(data);
};
test();
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{data && data.data}
</div>
);
}
Here is a similar example using class components.
import React from "react";
import "./styles.css";
const doThing = async () => {
console.log("doing a thing");
return { message: "New message" };
};
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: "Default message"
};
}
componentDidMount() {
const test = async () => {
const data = await doThing();
console.log("after", data.message);
this.setState({
data: data.message
});
};
test();
}
render() {
console.log("rendering", this.state.data);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{this.state.data}
</div>
);
}
}
Hope that helps!
Currently your code contains an async function mixed up with HTML elements.
Instead you have React's state that'll take care of the re-rendering once the data is fetched:
const ModalWrapper = ({ id }) => {
const [myImages, setMyImages] = React.useState([]);
const fetchImages = async (id) => {
try {
const newImages = await fetchImagesCall(id);
setMyImages(newImages); // Will trigger a re-render of the component, with the new images
} catch (e) {
console.log("Failed")
}
}
React.useEffect(() => {
fetchImages(id);
}, []); // This empty array makes sure the `fetchImages` call will occur only once the component is mounted
return (
<Modal>
<div className="item">
<div>
{myImages.map((item, key) =>
<p>Image name: {item.name}, En device name: {item.name.en_US}</p>
)}
</div>
</div>
</Modal>
);
}
Related
I have a search component where the search result updates according to the search input, where if there is data returned from the API, it is rendered as a book grid, if there is no data, a message is displayed, and if the search input is empty, nothing is rendered.
My problem is that when query state updates the searchResult state does update but when I delete the search input so fast (make the search input empty), query becomes updates as an empty string but searchResult does not update according to it. What could be the problem?
Here is the code to the search component: (Note: I tried the componentDidUpdate() method and the setState() callback function but nothing worked)
import React, { Component } from "react";
// import "React Router" components
import { Link } from "react-router-dom";
// import custom components
import Book from "./Book";
// import required API
import * as BooksAPI from "../BooksAPI";
export default class BookSearch extends Component {
state = {
query: "",
searchResult: [],
};
handleInputChange = (query) => {
this.setState(
{
query: query.trim(),
},
() => {
if (query) {
console.log(query);
BooksAPI.search(query).then((books) => {
this.setState({ searchResult: books });
});
} else {
this.setState({ searchResult: [] });
}
}
);
};
// componentDidUpdate(currentProps, currentState) {
// if (currentState.query !== this.state.query && this.state.query) {
// BooksAPI.search(this.state.query).then((books) => {
// this.setState({ searchResult: books });
// });
// } else if (currentState.query !== this.state.query && !this.state.query) {
// this.setState({ searchResult: [] });
// }
// }
render() {
const { query, searchResult } = this.state;
const { updateBookShelves } = this.props;
return (
<div className="search-books">
<div className="search-books-bar">
<Link to="/" className="close-search">
Close
</Link>
<div className="search-books-input-wrapper">
<input
type="text"
placeholder="Search by title or author"
value={query}
onChange={(event) => this.handleInputChange(event.target.value)}
/>
</div>
</div>
<div className="search-books-results">
<ol className="books-grid">
{ searchResult.error ?
<p>No results matching your search</p>
: searchResult.map((book) => (
<Book
key={book.id}
book={book}
updateBookShelves={updateBookShelves}
/>
))
)
) )}
</ol>
</div>
</div>
);
}
}
I am not 100% sure about this solution since it is using setState inside callback of other setState but you can give it a try.
I think you can probably need to use setTimeout before calling api for data and before checking if query exist or not we can set timeout to null so it will not call unwanted api calls.
handleInputChange = query => {
this.setState(
{
query: query.trim()
},
() => {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (query) {
this.timeout = setTimeout(() => {
BooksAPI.search(query).then(books => {
this.setState({ searchResult: books });
});
}, 800);
} else {
this.setState({ searchResult: [] });
}
}
);
};
I'm trying to render an html element to render based on the return value of an async/await function. I can't seem to get it working correctly.
Below is the async function being called
const isAuthorized= async() => {
const isAuthorized = await Promise.resolve().then(() => false);
console.log("isAuthorized =", isAuthorized);
return isAuthorized;
}
Below is the jsx:
const ComponentName= () => {
return (
<div>
{Promise.resolve(isAuthorized()).then(res => {res ? <p>User is authorized</p> : <p>User is not authorized</p>})}
</div>
)
}
export default ComponentName;
This is the error I'm getting:
You need to keep a state in your component to track whether your user is authorized or not. Then you can use a useEffect hook to check the status. Your component should render (and re-render) based on the state change.
https://codesandbox.io/s/promiseinuseeffect-do22b?file=/src/App.js
It is imposible in React.
1) Try use async/await
2) Make you preparation data in useEffect ( componendDidUpdate )
const ComponentName = () => {
const [isAuth, setIsAuth] = setState(undefined);
React.useEffect(() => {
(async function() {
if (!isAuth) {
const result = await isAuthorized();
setIsAuth(result);
}
})();
}, []);
const text = isAuth ? 'User is authorized' : 'User is not authorized';
return (
<div>
<p>{text}</p>
</div>
);
};
I was able to get it working using state
class ComponentName extends Component {
constructor(props) {
super(props);
this.state= {
isAuthorized: false
}
}
async componentDidMount(){
const isAuthorized = await Promise.resolve().then(() => false);
this.setState({isAuthorized: isAuthorized});
}
render() {
return (
<div>
{this.state.isAuthorized ? <p>User is authorized</p> : <p>User is not authorized</p>}
</div>
)
}
}
However, I don't think it's secure having a variable like this as state on the dom?
My goal is to make an API request when user types something on input. I'm getting the data successfully. However the component is rerendering twice and giving me this warning. If I include 'context' I'm getting an infinite loop. Here's my code:
Component.js:
const SearchBox = () => {
const [searchTerm, setSearchTerm] = useState("");
const { handleSearch, searchResults } = useContext(MovieContext);
console.log(searchResults);
useEffect(() => {
let timer;
timer = setTimeout(() => {
handleSearch(searchTerm);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm]);
const renderResults = () => {
if (searchResults.length > 0) {
searchResults.map(result => {
return (
<div key={result.Title}>
<img src={result.Poster} alt={result.Title} />;
</div>
);
});
}
return;
};
return (
<>
<label>
<b>Search</b>
</label>
<input
className="input"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
<div className="dropdown is-active">
<div className="dropdown-menu">
<div className="dropdown-content results">{renderResults()}</div>
</div>
</div>
</>
);
};
On top of this context.searchResults is undefined, although I set the initial value as an empty array. I wanted to know what causing this. What am I doing wrong? Here is my context code below:
Context.js:
const Context = React.createContext("");
export class MovieStore extends Component {
constructor(props) {
super(props);
this.state = {
searchResults: [],
handleSearch: this.handleSearch
};
}
handleSearch = async term => {
try {
if (term !== "") {
const response = await axios.get("http://www.omdbapi.com/", {
params: {
apikey: apikey,
s: term
}
});
this.setState({ searchResults: response.data.Search });
}
} catch (error) {
console.log(error);
}
};
render() {
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
);
}
}
Exactly the same thing about an infinite loop is mentioned in React docs here. So the cause of infinite loop is that, in context render function, you create new value every time render is called.
render() {
return (
<Context.Provider
// ! value object creates every time render is called - it's bad
value={{ ...this.state, handleSearch: this.handleSearch }}
>
{this.props.children}
</Context.Provider>
);
}
It causes every consumer to rerender when context state updates. So, if you put context in dependencies array of useEffect, eventually, it'll cause an infinite loop, because context value is always different. Here's what happens:
Context makes a search query.
Context state updates with the new data, which causes all consumers to rerender.
In context consumer useEffect sees that context value has been
updated and it calls setTimeout which will call for another search
in context provider in 500ms.
Consumer calls context to make another search query and we've got an infinite loop!
The solution is to keep context value the same object, while only updating its properties. It can be done by putting all the necessary properties inside of context state. Like that:
export class MovieStore extends Component {
handleSearch = async term => {
try {
if (term !== "") {
const response = await axios.get("http://www.omdbapi.com/", {
params: {
apikey: "15bfc1e3",
s: term
}
});
this.setState({ searchResults: response.data.Search });
}
} catch (error) {
console.log(error);
}
};
state = {
searchResults: [],
handleSearch: this.handleSearch // <~ put method directly to the state
};
render() {
return (
<Context.Provider value={this.state}> // <~ Just returning state here
{this.props.children}
</Context.Provider>
);
}
}
Hope it helps <3
Im trying to create recipes searcher. In App.js I receive query from search input from another component and I want to setState to answer from APi. Console.log from callback in setState shows updated state but the state is not updated. I need setState updaed so I can use map on it and display list of recipes in render. It gives me error map is not a function because this.state.recipesList is still empty. Anyone can help me ?
class App extends Component {
state = {
query: "",
recipesList: []
};
getQuery = query => {
const key = "2889f0d3f51281eea62fa6726e16991e";
const URL = `https://www.food2fork.com/api/search?key=${key}&q=${query}`;
fetch(URL)
.then(res => res.json())
.then(res => {
this.setState(
{
recipesList: res
},
() => {
console.log(this.state.recipesList);
}
);
});
console.log(this.state.recipesList);
};
render() {
const test = this.state.recipesList.map(item => {
return (
<div className="recispesList">
<h1>{item.title}</h1>
</div>
);
});
return (
<div className="App">
<Search query={this.getQuery} />
<div className="contentWrapper">{}</div>
</div>
);
}
}
Search component:
class Search extends Component {
state = {
searchValue: ""
};
handleChange = val => {
let searchValue = val.target.value;
this.setState({
searchValue
});
};
handleSubmit = e => {
e.preventDefault();
this.setState({
searchValue: ""
});
this.props.query(this.state.searchValue);
};
render() {
return (
<div className="searchWrapper">
<form onSubmit={this.handleSubmit}>
<input onChange={this.handleChange} value={this.state.searchValue} />
<button />
</form>
</div>
);
}
}
export default Search;
It seems that instead of directly assigning the whole response to recipesList:
this.setState(
{
recipesList: res
},
() => {
console.log(this.state.recipesList);
}
);
you need to get recipes array first via res.recipes:
this.setState(
{
recipesList: res.recipes
},
() => {
console.log(this.state.recipesList);
}
);
I am bulding an app using newsapi. i am facing two issue on my state. i fetch data using api and assign it to my state. and use it in my view.
Issue no 1
My view gets rendered before my app receives the data.
Issue no 2
When I try to update my state after a new fetch. it recursively updates the set of data again and again.
import React, {Component} from 'react';
import NewsComponent from './NewsComponent/NewsComponent'
class News extends Component {
state = {
displayStatus: false,
newsItems: []
};
toogleDisplayHandler = () => {
if(this.state.displayStatus===true){
this.setState({displayStatus:false})
}
else{
this.setState({displayStatus:true})
}
}
render(){
const NewsAPI = require('newsapi');
const newsapi = new NewsAPI('d6da863f882e4a1a89c5152bd3692fb6');
//console.log(this.props.keyword);
newsapi.v2.topHeadlines({
sources: 'bbc-news,abc-news',
q: this.props.keyword
}).then(response => {
//console.log(response)
response.articles.map(article => {
//console.log(article);
return(
//console.log(this.state.newsItems)
this.setState({
newsItems: [...this.state.newsItems, article],
})
//this.state.newsItems.push(article)
)
});
});
let Article = null;
Article = (
<div>
{
this.state.newsItems.map((news, index) => {
return (
<NewsComponent key={index}
title={news.title}
url={news.url}
description={news.description}
author={news.author}
publish={news.publishedAt}
image={news.urlToImage}
/>
)
})
}
</div>
)
return (
<div className="App">
{Article}
<button onClick={this.toogleDisplayHandler}>
{this.state.displayStatus === true ? "Hide Article" : "Display Articles"}
</button>
</div>
)
}
}
export default News;
Please help me to resolve this issue.
You should never setState in render as that would cause an infinite loop. Do it in componentDidMount or the constructor.
I would also recommend not using map for simply iterating over a list. Array.map is a function that is useful for returning an array that is constructed by iterating over another array. If you want to run some code for each element of an array use Array.forEach instead.
Like this:
import React, { Component } from "react";
import NewsComponent from "./NewsComponent/NewsComponent";
class News extends Component {
state = {
displayStatus: false,
newsItems: []
};
toogleDisplayHandler = () => {
if (this.state.displayStatus === true) {
this.setState({ displayStatus: false });
} else {
this.setState({ displayStatus: true });
}
};
componentDidMount = () => {
const NewsAPI = require("newsapi");
const newsapi = new NewsAPI("d6da863f882e4a1a89c5152bd3692fb6");
newsapi.v2
.topHeadlines({
sources: "bbc-news,abc-news",
q: this.props.keyword
})
.then(response => {
response.articles.forEach(article => {
this.setState({
newsItems: [...this.state.newsItems, article]
});
});
});
};
render() {
let Article = null;
Article = (
<div>
{this.state.newsItems.map((news, index) => {
return (
<NewsComponent
key={index}
title={news.title}
url={news.url}
description={news.description}
author={news.author}
publish={news.publishedAt}
image={news.urlToImage}
/>
);
})}
</div>
);
return (
<div className="App">
{Article}
<button onClick={this.toogleDisplayHandler}>
{this.state.displayStatus === true
? "Hide Article"
: "Display Articles"}
</button>
</div>
);
}
}
export default News;
1) You can add a check either your state has the data which you want to show on screen to render the view.
2) Please use ComponentDidMount React life cycle function to fetch data from an external source and update this data in the state. In the Render method, it will keep calling it recursively.