I need to listen to changes in the state, and if I change it, pass it to the props, but when I try to make the setState an infinite loop and an error begins. Is it possible to make a condition so that there is no error or can there be another way to transfer data from the component?
ProfileTag.js
import React, {Component} from 'react';
import classnames from 'classnames';
import './ProfileTag.scss';
import Tag from './Tag';
class ProfileTag extends Component {
state = {
tags: this.props.tags
}
onKeyUp = (e) => {
// Space -> 32 and enter is 13
if (e.which === 32 || e.which === 13) {
let input = e.target.value.trim().split(" ")[0];
if (input.length === 0 || input[0] === "") return; // empty tags
this.setState({
tags: [...this.state.tags, input]
});
e.target.value = "";
}
}
onDeleteTag = (tag) => {
const tags = this.state.tags.filter((t) => {
return (t !== tag);
});
// console.log("tags: ", tags);
this.setState({tags: tags});
}
componentWillUpdate(nextProps, nextState) {
console.log(nextProps)
console.log(nextState)
this.props.getTags(nextState.tags);
}
render() {
const tags = this.state.tags.map((tag, i) => {
return <Tag
onDeleteTag={this.onDeleteTag}
key={i}
value={tag}/>
});
return (
<div className="ProfileTag">
<label className="ProfileTag__title"
htmlFor={this.props.name}>{this.props.label}</label>
<input
onKeyUp={(e) => this.onKeyUp(e)}
type={this.props.type}
className={classnames('ProfileTag__input')}
placeholder={this.props.placeholder}
name={this.props.name}
id={this.props.name}
value={this.props.value}
disabled={this.props.disabled}
/>
<ul className="ProfileTag__tags">{tags}</ul>
</div>
)
}
}
export default ProfileTag;
Here error:
import React, {Component, Fragment} from 'react';
import connect from 'react-redux/es/connect/connect';
import ProfileTag from '../common/Tag/ProfileTag';
class Profile extends Component {
constructor() {
super();
this.state = {};
}
getTags(tags) {
console.log(tags)
// Here error
this.setState({tata: 'vava'});
}
render() {
let profileContent = (
<div className="Profile">
<ProfileTag
label="Введите теги (ваша основная специальность)"
placeholder="Тег"
name="test1"
tags={this.state.profile.tags}
getTags={this.getTags.bind(this)}
/>
</div>
);
return (
<Fragment>
{profileContent}
</Fragment>
);
}
}
const mapStateToProps = state => ({
profile: state.profile
});
export default connect(mapStateToProps, {getCurrentProfile, postProfile})(Profile);
Two things you should change
componentWillUpdate is deprecated in latest versions, use componentDidUpdate instead
Do not call a function from componentDidUpdate which calls setState directly otherwise your code will go into an infinite loop
Code example
componentDidUpdate(prevProps, prevState) {
if(prevState.tags !== this.state.tags) { // you can use isEqual from lodash for a equality check on arrays or objects
this.props.getTags(this.state.tags);
}
}
in componentDidUpdate ,never use setState,every time when it changes state which would be continuous ,it will go in infinite loop. You can check the with prevprops in componentDidUpdate(object prevProps, object prevState), and make sure you have new props in your hand.
Outside of your componentdidupdate ,create a callback function and with prevprops you can setstate there.
Related
I have the following component:
import React from 'react';
import './styles.css';
import ToolTip from '../../Common/components/ToolTip/ToolTip';
export default class RouteTitleTooltipComponent extends React.Component {
constructor(props) {
super(props);
this.titleParagraphRef = React.createRef();
this._tooltipTimer = null;
this.state = { shouldPopupBeEnabled: false, isTooltipShown: false };
this._showTooltip = this._showTooltip.bind(this);
this._hideTooltip = this._hideTooltip.bind(this);
}
componentDidMount() {
const { scrollWidth, clientWidth } = this.titleParagraphRef.current;
const shouldPopupBeEnabled = scrollWidth > clientWidth;
this.setState({ shouldPopupBeEnabled });
}
_showTooltip() {
this._tooltipTimer = setTimeout(
() => {
this.setState({ isTooltipShown: true });
}, 1000,
);
}
_hideTooltip() {
clearTimeout(this._tooltipTimer);
this.setState({ isTooltipShown: false });
}
render() {
const { shouldPopupBeEnabled, isTooltipShown } = this.state;
const { message } = this.props;
return (
<ToolTip
message="Tooltip!!"
popoverOpen={shouldPopupBeEnabled && isTooltipShown}
>
<div
ref={this.titleParagraphRef}
onMouseOver={this._showTooltip}
>
{message}
</div>
</ToolTip>
);
}
}
This basically renders a floating tooltip over a div element if the message inside of it is bigger than the container. To do that, I use scrollWidth and clientWidth of the div element using a React reference. To detect those values I use componentDidMount, but this only works in full renders of the component. That is, if I have the component visible and reload the page, both values are equal to 0 and it does not work.
In addition, if I change the message, it does not work either because the component is already mounted.
So what I want is to change the state right after the component is mounted or updated so that the react reference is rendered and clientWidth and scrollWidth are not 0.
I have tried replace componentDidUpdate instead of componentDidMount but it's not a good practica to use setState inside componentDidUpdate.
Any solution?
First you should know that componentDidMount will execute only once. Therefor you can go for componentDidUpdate but don't forget to put a condition as it will render in a loop.
componentDidUpdate(prevProps,prevState) {
const shouldPopupBeEnabled = scrollWidth > clientWidth;
if (shouldPopupBeEnabled !== this.state.shouldPopupBeEnabled ) {
this.setState({shouldPopupBeEnabled });
}
}
Or you can go for functional components and use useEffect which will only render again if state changes.
useEffect(() => {
console.log('mounted');
}, [shouldPopupBeEnabled]) // It will re render id `shouldPopupBeEnabled` changes
I have a single page create-react-app project and the issue I am having is that the state is not updating. I have cut some of my code out just to make the example a bit shorter and easier to follow. The function where setState() is called is in form_change().
My goal here is to change the color of the text when there is an error, but even {this.state.test} isn't updating. I have tried putting console.log()s in various locations to get around the async nature of setState, but unfortunately they seem to show that state is never updated. It has been a while since I have used React, so there is a chance I am doing something very silly!
Thanks in advance for your help.
Here is my code:
import React,{Component} from 'react';
import './App.css';
import Dropdown from 'react-dropdown'
import classes from './classes.module.css'
import firebase from 'firebase';
import 'react-dropdown/style.css';
const axios = require('axios');
class App extends Component {
render(){
const error_empty = (param)=>{
if (this.state.error===undefined){
return false
}
else{
if (this.state.errors.find(el => el === param) === undefined){
return false
}
else return true
}
}
const form_change = (event, param)=>{
let form={...this.state.form};
form[param]=event.target.value;
let errors =verified(form);
console.log(form); //as expected
console.log(errors); //as expected
//works up til here. setState not updating for some reason.
this.setState({form:form,errors:errors,test:'Hello World'})
}
const verified = (data)=>{
let errors = [];
let form = data;
errors.push('ean')
return errors}
this.state = {
example:['abc'],
form:{
example:"abc"
}
}
return (
<div className="App">
<header className="App-header">
<div className={classes.button_holder}>
<div className={classes.page_button} onClick={()=>{null}}>
{this.state.test}
</div>
</div>
<div className={classes.user_form}>User Update Form
<div>
<input className={classes.input_text} style={{color: error_empty()?'red':'black'}} value={this.state.form.ean} onChange={(event)=>{form_change(event,'ean')}} placeholder={"EAN"}></input>
</div>
</div>
</header>
</div>
);
}
}
export default App;
Move initialization of state outside render function this.state.
Initialise your state in constructor
Don't update your state (this.setState) in render because this will lead to infinitive loop.
Move your functions error_empty(), form_change() and verified() outside the render.
To call function onChange use this
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
example:['abc'],
form:{
example:"abc"
}
error: ""
}
}
error_empty = (param) => {
if(this.state.error===undefined) {
return false
}
else {
if(this.state.errors.find(el => el === param) === undefined) {
return false
}
return true
}
}
...
render() {
...
<input ... onChange={(event) => {this.form_change(event,'ean')}}/>
...
}
}
I have an app with one child component that I would like to re-render when setState updates the bookInput in the parent's state. I am using axios to request info from google's book api. For some reason, even though the state is updating, the child is not re-rendering. Please help if you can! Thank you!
import React, { Component } from 'react';
import axios from 'axios';
class App extends Component {
constructor(props) {
super(props);
this.state = {
bookInput: 'ender',
bookSubmitted: 'initial'
}
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleSubmitEmpty = this.handleSubmitEmpty.bind(this);
}
handleChange(e) {
this.setState({bookInput: e.target.value});
console.log(this.state.bookInput);
//this.setState({bookSubmitted: false});
}
handleSubmit(e) {
e.preventDefault();
//this.setState({bookSubmitted: true})
const name = this.state.bookInput;
this.setState({bookInput: name});
console.log(this.state);
this.setState({bookSubmitted: 'userSub'});
}
handleSubmitEmpty(e) {
alert('please enter an item to search for');
e.preventDefault();
}
render() {
return (
<div className="App">
<header className = "App-header">
<h1>Book Search App</h1>
</header>
<form className = "form-style" onSubmit = {this.state.bookInput ? this.handleSubmit: this.handleSubmitEmpty}>
<label>
<input type="text" className = "input-style"
value = {this.state.bookInput} onChange = {this.handleChange}>
</input>
</label>
<button type="submit">search books</button>
</form>
{/* <Book bookInput = {this.state.bookInput}/> */}
{/*this.state.bookSubmitted && <Book bookInput = {this.state.bookInput}/>*/}
{
(this.state.bookSubmitted === 'initial' || this.state.bookSubmitted === 'userSub') &&
<Book bookInput = {this.state.bookInput}/>
}
</div>
);
}
}
export default App;
class Book extends Component {
constructor(props) {
super(props);
this.state = {
//bookInput2: "ender",
bookTitles: [],
bookExample: '',
isLoading: false
}
this.bookClick = this.bookClick.bind(this);
}
bookClick(book) {
console.log(book);
console.log(book.volumeInfo.infoLink);
const bookURL = book.volumeInfo.infoLink;
window.open(bookURL);
}
componentDidMount() {
//this.setState({ isLoading: true });
this.setState({isLoading: true});
axios.get(`https://www.googleapis.com/books/v1/volumes?q=${this.props.bookInput}`)
.then((response) => {
const bookExample1 = response.data.items;
console.log(bookExample1);
this.setState({bookTitles: bookExample1, isLoading: false});
})
.catch((error) => {
console.error('ERROR!', error);
this.setState({isLoading: false});
});
}
render() {
return (
<div>
{ this.state.bookTitles ? (
<div>
<h2>book list</h2>
{<ul className = 'list-style'>
{this.state.isLoading &&
(<div>
loading book list
</div>)
}
{this.state.bookTitles.map(book => (
<li key={book.id}>
<span className = 'book-details book-title' onClick = {() => this.bookClick(book)}> {book.volumeInfo.title}</span>
<br/>
{book.volumeInfo.imageLinks &&
<img src = {book.volumeInfo.imageLinks.thumbnail}/>
}
{ book.volumeInfo.description &&
<span className = 'book-details'>{book.volumeInfo.description}</span>
}
<br/>
<span className = 'book-details'>Categories {book.volumeInfo.categories}</span>
</li>
))}
</ul>}
</div>) :
(<p>sorry, that search did not return anything</p>)}
</div>
);
}
}
May be you are looking for something similar to this?
https://stackblitz.com/edit/react-snoqkt?file=index.js
The above code can be simplified more and organized but it gives you some idea.
Main changes in the code.
Changed Api call from componentDidMount lifecycle event to a new method named getInitialdata which is called in handleSubmit.
getInitialdata(name){
axios.get(`https://www.googleapis.com/books/v1/volumes?q=${name}`)
.then((response) => {
const bookExample1 = response.data.items;
console.log(bookExample1);
this.setState({bookTitles: bookExample1, isLoading: false, bookSubmitted: 'userSub'});
})
.catch((error) => {
console.error('ERROR!', error);
this.setState({isLoading: false, bookSubmitted: 'userSub'});
});
}
Changed the way how Child component is used.
<Book bookTitles={this.state.bookTitles} isLoading={this.state.isLoading}/>
Issue with your code is you are making an API call in your component's didMount method. This lifecycle event will be invoked only when the component is mounted. Not when it is updated.
When you enter some input in your textbox and click on "Search books", componentDidMount event doesnt fire. And this is the reason why API calls are not happening from the second time.
More on the lifecycle events at https://reactjs.org/docs/react-component.html#componentdidmount
I've taken your code and extrapolated it into this sandbox. Just as you said, your parent component state is updating as it should, but the problem is that the child component doesn't change its state.
A state change will always trigger a re-render in React. The only problem is, your child component is managing it's own state, which isn't directly changing. Instead, it's just receiving new props again and again, but not doing anything with them.
If you look at your code for the <Book /> component, you only modify its state on componentDidMount, which only happens once. If you'd like to programmatically make it update, you can do one of two things.
Remove state from the child component, and make it rely entirely on props, so that it stays in sync with the parent
Use the componentDidUpdate lifecycle method (docs) to choose when to change the state of the child (which will trigger the re-render)
This error :
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
why is this error?
import React, { Component } from 'react';
import './App.css';
class App extends Component {
constructor(props){
super(props);
this.state=({
todos:[],
})
this.add = this.add.bind(this);
this.remove = this.remove.bind(this);
}
//this.array.splice(2,1)
/*
let arrayy = [...this.state.array]
let removed = arrayy.splice(deger,1);
this.setState({
array:arrayy,
})
*/
add(){
const deger = document.getElementById('deger').value;
const todosarray = this.state.todos;
todosarray.push(deger)
this.setState({
todos:todosarray,
})
}
remove(num){
let arrayy = [...this.state.todos]
arrayy.splice(num,1)
this.setState({
array:arrayy,
})
}
render() {
const data = this.state.todos;
const listItems = data.map((result,i) => <li key={i} onClick={this.remove(i)}>{result }</li>);
return (
<div >
<input id="deger"></input>
<button onClick={this.add}>Add</button>
<div id="items">
{listItems}
</div>
</div>
);
}
}
export default App;
Replace this:
onClick={this.remove(i)}
By this:
onClick={() => this.remove(i)}
Explanation: while rendering, React evaluates this.remove(i), which changes the state or the props, thus triggering another render, and looping to re-evaluate this.remove(i); creating a (hidden) infinite loop. () => this.remove(i) is a function so the state or the props do not change. Also, it's probably what you wanted to code anyway ;)
I have a component which receives a list of news from two or three APIs. the first time the component renders, the apis are called and data is rendered in componentDidMount
something like this:
componentDidMount() {
this.state.platforms.forEach((platform, i) => {
let objToSend = {
phrase: this.props.searchParams.phrase,
// this is the main place when input data changes
...this.props.searchParams.general_params,
...this.props.searchParams.platforms[i].api_params,
stringPath: platform,
apiPath: this.props.searchParams.platforms[i].apiPath,
}
this.props.loadData(objToSend)
// this is the api call using redux which sends data as props to this component
}
new when the phrase change, I want this component to re-render and re-run the componentDidMount, but it's not working because componentDidMount will run once.
So I used componentDidUpdate, but as there are a number of calls so the api is being updated continuesly.
How can I make the component re-render and re-run componentDidMount every time I change the phrase
You can use componentDidUpdate arguments (previousProps, previousState) to check whether some new changes happened.
Example
componentDidUpdate(previousProps, previousState) {
if (previousProps.phrase !== this.props.phrase) {
//Do whatever needs to happen!
}
}
I stopped the infinite loop for my situation this way.
Here's one way to do something() when you re-render.
import React, { Component } from 'react';
import { render } from 'react-dom';
const fakeFetch = (n) => {
console.log(`Doing fake fetch: ${n}`)
return n
}
class App extends Component {
state = {
value: false,
number: 0,
}
componentDidMount() {
const number = fakeFetch(this.state.number + 1);
this.setState({ number })
}
componentDidUpdate(prevProps, prevState) {
if (prevState.value !== this.state.value) {
const number = fakeFetch(this.state.number + 1);
this.setState({ number })
}
}
toggle = () => {
this.setState(({ value }) => ({ value: !value }))
}
render() {
return (
<React.Fragment>
<h1>Number: {this.state.number}</h1>
<button onClick={this.toggle}>re-render</button>
</React.Fragment>
);
}
}
render(<App />, document.getElementById('root'));
Live example here.