I'm using an API called 'react-svg', it allows me to inject SVG file into the DOM and also have a copy of the SVG element so I can use it later.
This API has a functionality called 'afterInjection', it's basically a function called after injecting the SVG file, inside that function I tried to update the component state and I got an error :
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.
import { ReactSVG } from 'react-svg';
import Sketch from '../sketch/Sketch';
import './ImageUploader.scss';
import Complex from 'complex.js';
class ImageUploader extends Component {
constructor( props ) {
super( props );
this.state = {
pointsArray : [],
fileURL : null,
file: null
}
this.handleChange = this.handleChange.bind(this);
}
handleChange = (event) => {
this.setState({
file : event.target.files[0],
fileURL: URL.createObjectURL(event.target.files[0])
})
}
add_svg = () => {
if (this.state.fileURL !== null) {
return (
<ReactSVG
src={this.state.file.name}
afterInjection={(error, svg) => {
if (error) {
console.error(error)
return
}
this.convert_path( svg.children[0].children[0] );
}}
/>
);
}
return null;
}
convert_path = path => {
let points = [];
const length = path.getTotalLength();
const step = length / 100;
for (let i = length - 1; i >= 0; i -= step) {
// console.log(path.getPointAtLength(i));
points.push( new Complex( path.getPointAtLength(i).x, path.getPointAtLength(i).y) );
}
this.setState({ pointsArray : points });
}
render() {
return (
<div >
<div className="upload_sketch_container">
<div className="input_container">
<input type="file" id="file" onChange={this.handleChange} />
<label htmlFor="file">Upload SVG file</label>
</div>
<div className="image_sketch_container">
<div className="image_container" >
{this.add_svg()}
</div>
<div className="sketch_container">
<Sketch data={this.state.pointsArray} />
</div>
</div>
</div>
</div>
);
}
}
export default ImageUploader;
The error message you posted is exactly correct. You've created an infinite loop in your component. You render your component, which calls add_svg, which calls convert_path, which updates state, which triggers a rerender, which calls add_svg, etc. If you coment out the call to this.convert_path in add_svg, your infinite loop problem will vanish.
Related
import React, {Component} from 'react';
import "./DisplayCard.css";
class DisplayCard extends Component {
runArray = (array) => {
for (var i = 0; i<array.length; i++) {
return <div>{array[i].task}</div>
}
}
renderElements = (savedTasks) =>{
if (savedTasks.length === 0) {
return <div className="noTasks"> <p>You have no saved tasks.</p> </div>
} else {
return this.runArray(savedTasks)
}
}
render() {
return (
<div className="DisplayCardContainer">
{this.renderElements(this.props.saved)}
</div>
)
}
}
export default DisplayCard;
Hey guys,
I am new to react, so this is my child component that takes state from its parent component. My goal is to re-render component every time the array this.props.saved is changed.
This component renders: <p>You have no saved tasks.</p> when the this.props.saved.length === 0 and it renders <div>{array[0].task}</div> when i enter the first task, but it keeps it at <div>{array[0].task}</div> after that. I do see that the state keeps changing and this.props.saved keeps getting bigger, but my component doesn't change anymore.
Here's your problem:
runArray = (array) => {
for (var i = 0; i<array.length; i++) {
//the first time we get here, it immediately ends the function!
return <div>{array[i].task}</div>
}
}
This loop only ever goes through once (at i=0) and then returns, exiting the runArray function and cancelling the rest of the loop. You probably wanted to return an array of elements, one for each of the tasks. I recommend using Array.map() for this, which takes an array and transforms each element, creating a new array:
runArray = (array) => {
return array.map(arrayElement => <div>arrayElement.task</div>);
}
This should do the trick. Note that React may complain about the fact that your elements lack the key property - see the documentation for more info: https://reactjs.org/docs/lists-and-keys.html
The problem is in your runArray function. Inside your loop, you are returning the first element and that's it. My guess is, you see only the first entry?
When you are trying to render all your tasks, I would suggest to map your tasks, e.g.
runArray = (array) => array.map(entry => <div>{entry.task}</div>)
It is because you write wrong the runArray function. You make a return in the for loop so it breaks after the first iteration. It will not iterate over the full array.
You need to transform your for loop to a map : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
runArray = (array) => {
return array.map(v => <div>{v.task}</div>)
}
Does it fix your issue ?
You have to update state of the component to trigger render function. Your render function is not triggered because you did not update the state when the props changed. There are many ways to update state when props updated. One method may be the following:
componentWillReceiveProps(nextProps){
if (nextProps.saved !== this.props.saved) {
this.setState({ saved: nextProps.saved })
}
}
Also change yoour render function to use state of the component as below:
renderElements = () =>{
if (this.state.savedTasks.length === 0) {
return <div className="noTasks"> <p>You have no saved tasks.</p> </div>
} else {
return this.runArray(this.state.savedTasks)
}
}
Use .map so that it renders your task correctly. You can remove runArray and rely entirely on props so you don't need to pass arguments across functions as it can get messy quickly. Here's a quick running example of how to create a parent component where you can add a task and pass them into a component so that it renders your data when props are changed, therefore making it reactive.
class App extends React.Component {
state = {
taskLabel: "",
tasks: [
{
id: 1,
label: "Do something"
},
{
id: 2,
label: "Learn sometihng"
}
]
};
handleInput = evt => {
this.setState({
[evt.target.name]: evt.target.value
});
};
handleSubmit = evt => {
evt.preventDefault();
this.setState(prevState => ({
taskLabel: "",
tasks: [
...prevState.tasks,
{
id: prevState.tasks.length + 1,
label: this.state.taskLabel
}
]
}));
};
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input
name="taskLabel"
type="text"
placeholder="Task label"
value={this.state.taskLabel}
onChange={this.handleInput}
/>
<button>Create task</button>
</form>
<DisplayCard tasks={this.state.tasks} />
</div>
);
}
}
class DisplayCard extends React.Component {
renderTasks = () => {
if (this.props.tasks.length !== 0) {
return this.props.tasks.map(task => (
<div key={task.id}>{task.label}</div>
));
} else {
return <div>No tasks</div>;
}
};
render() {
return <div>{this.renderTasks()}</div>;
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I'm trying to generate several divs based off an array - but I'm unable to. I click a button, which is supposed to return the divs via mapping but it's returning anything.
class History extends Component {
constructor(props) {
super(props);
this.state = {
info: ""
};
this.generateDivs = this.generateDivs.bind(this);
}
async getCurrentHistory(address) {
const info = await axios.get(`https://api3.tzscan.io/v2/bakings_history/${address}?number=10000`);
return info.data[2];
}
async getHistory() {
const info = await getCurrentHistory(
"tz1hAYfexyzPGG6RhZZMpDvAHifubsbb6kgn"
);
this.setState({ info });
}
generateDivs() {
const arr = this.state.info;
const listItems = arr.map((cycles) =>
<div class="box-1">
Cycle: {cycles.cycle}
Count: {cycles.count.count_all}
Rewards: {cycles.reward}
</div>
);
return (
<div class="flex-container">
{ listItems }
</div>
)
}
componentWillMount() {
this.getHistory();
}
render() {
return (
<div>
<button onClick={this.generateDivs}>make divs</button>
</div>
);
}
You are not actually rendering the the divs just by invoking the generateDivs function, the JSX it is returning is not being used anywhere.
To get it to work you could do something like -
render() {
return (
<div>
<button onClick={this.showDivs}>make divs</button>
{this.state.isDisplayed && this.generateDivs()}
</div>
);
}
where showDivs would be a function to toggle the state property isDisplayed to true
The main point is that the JSX being returned in the generateDivs function will now be rendered out in the render function. There is many ways to toggle the display, that is just one straight forward way
I'm trying to figure out how to render out a set of divs, without re-rendering the entire list as a new set is added.
So I've got a stateful component. Inside said stateful component, I've got a function that A, gets a list of post id's, and B, makes a request to each of those post id's and pushes the results to an array. Like so:
getArticles = () => {
axios.get(`${api}/topstories.json`)
.then(items => {
let articles = items.data;
let init = articles.slice(0,50);
init.forEach(item => {
axios.get(`${post}/${item}.json`)
.then(article => {
this.setState({ articles: [...this.state.articles, article.data]});
});
})
});
}
Then, I've got a second function that takes this information and outputs it to a list of posts. Like so:
mapArticles = () => {
let articles = this.state.articles.map((item, i) => {
let time = moment.unix(item.time).fromNow();
return(
<section className="article" key={i}>
<Link className="article--link" to={`/posts/${item.id}`}/>
<div className="article--score">
<FontAwesomeIcon icon="angle-up"/>
<p>{item.score}</p>
<FontAwesomeIcon icon="angle-down"/>
</div>
<div className="article--content">
<div className="article--title">
<h1>{item.title}</h1>
</div>
<div className="article--meta">
{item.by} posted {time}. {item.descendants ? `${item.descendants} comments.` : null}
</div>
</div>
<div className="article--external">
<a href={item.link} target="_blank">
<FontAwesomeIcon icon="external-link-alt"/>
</a>
</div>
</section>
)
});
return articles;
}
I then use {this.mapArticles()} inside the render function to return the appropriate information.
However, whenever the app loads in a new piece of data, it re-renders the entire list, causing a ton of jank. I.e., when the first request finishes, it renders the first div. When the second request finishes, it re-renders the first div and renders the second. When the third request finishes, it re-renders the first and second, and renders the third.
Is there a way to have React recognize that the div with that key already exists, and should be ignored when the state changes and the function runs again?
A technique that I use to only render the part that are new is to keep a cache map of already drawn obj, so in the render method I only render the new incoming elements.
Here is an example:
Take a look at https://codesandbox.io/s/wq2vq09pr7
In this code you can see that the List has an cache array and the render method
only draw new arrays
class RealTimeList extends React.Component {
constructor(props) {
super(props);
this.cache = [];
}
renderRow(message, key) {
return <div key={key}>Mesage:{key}</div>;
}
renderMessages = () => {
//let newMessages=this,props.newMessage
let newElement = this.renderRow(this.props.message, this.cache.length);
this.cache.push(newElement);
return [...this.cache];
};
render() {
return (
<div>
<div> Smart List</div>
<div className="listcontainer">{this.renderMessages()}</div>
</div>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = { message: "hi" };
}
start = () => {
if (this.interval) return;
this.interval = setInterval(this.generateMessage, 200);
};
stop = () => {
clearTimeout(this.interval);
this.interval = null;
};
generateMessage = () => {
var d = new Date();
var n = d.getMilliseconds();
this.setState({ title: n });
};
render() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={this.start}> Start</button>
<button onClick={this.stop}> Stop</button>
<RealTimeList message={this.state.message} />
</div>
);
}
}
If items arrive at the same time, wait till all items are fetched, then render:
getArticles = () => {
axios.get(`${api}/topstories.json`)
.then(items => {
let articles = items.data;
let init = articles.slice(0, 50);
Promise.all(init.map(item => axios.get(`${post}/${item}.json`)).then(articles => {
this.setState({
articles
});
})
});
}
If you really want to render immediately after an item is fetched, you can introduce a utility component that renders when promise resolves.
class RenderOnResolve extends React.Component {
state = null
componentDidMount() {
this.props.promise.then(data => this.setState(data))
}
render() {
return this.state && this.props.render(this.state);
}
}
// usage:
<RenderOnResolve promise={promise} render={this.articleRenderer}/>
This may be a quick fix but I have been racking my brain for the past little while, and could really use another set of eyes to take a look.
Basically I am trying to render an array full of generated JSX elements. I fell like I have done this a million times, but it does not seem to work here.
Heres the code:
import React, { Fragment } from 'react'
import css from './Search.scss';
import Header from '../SectionHeader/Header';
import SearchItem from '../SearchItem/SearchItem';
const Search = (props) => {
const { coinObject, coinKeys } = props;
let searchResults = []; // Array in question
const findResults = (searchText) => {
searchResults = []; // Reset the array to blank for each new character typed in input
for(let i = 0; i < coinKeys.length; i++) {
const { FullName } = coinObject[coinKeys[i]]; // App specific logic, not important, or the problem here
if(FullName.toLowerCase().includes(searchText) && (searchResults.length < 5)) {
console.log(FullName, searchText); // Prints the correct Full name based off of the searched text
searchResults.push(<SearchItem key={i} searchText={FullName} />);
}
}
console.log(searchResults); // Prints the updated array with all react elements
}
return (
<Fragment>
<Header title='Find Coins!' />
<div className={css.searchContainer}>
<div className={css.inputContainer}>
<input onChange={input => findResults(input.target.value)} className={css.searchInput} type='text' placeholder='Start Typing a Coin'/>
</div>
{ searchResults }
</div>
</Fragment>
);
}
export default Search;
And the SearchItem Component, which is super simple:
import React from 'react'
import css from './SearchItem.scss';
const SearchItem = (props) => {
return (
<div className={css.searchItem}>
{props.searchText}
</div>
)
}
export default SearchItem;
For a little bit of context, this component just gets a giant object of data, and will display the first 5 instances of what matches the input text. I am trying to make one of those search filter things, where as you type it suggests things that match from the data.
The array gets updated, and I can see the JSX objects in the array, they just do not render. I have a feeling it is due to the array not re-rendering?
Any help is much appreciated. Thanks!
You could make the Search component into a stateful component and store the searchResults in your state instead, so that when it is updated your component will be re-rendered.
Example
class Search extends React.Component {
state = { searchResults: [] };
findResults = searchText => {
const { coinObject, coinKeys } = this.props;
const searchResults = [];
for (let i = 0; i < coinKeys.length; i++) {
const { FullName } = coinObject[coinKeys[i]];
if (
FullName.toLowerCase().includes(searchText) &&
searchResults.length < 5
) {
searchResults.push(FullName);
}
}
this.setState({ searchResults });
};
render() {
return (
<Fragment>
<Header title="Find Coins!" />
<div className={css.searchContainer}>
<div className={css.inputContainer}>
<input
onChange={event => findResults(event.target.value)}
className={css.searchInput}
type="text"
placeholder="Start Typing a Coin"
/>
</div>
{this.state.searchResults.map((fullName, i) => (
<SearchItem key={i} searchText={fullName} />
))}
</div>
</Fragment>
);
}
}
When this component is called I get the follow error.
setState(...): Cannot update during an existing state transition (such
as within render or another component's constructor). Render methods
should be a pure function of props and state; constructor side-effects
are an anti-pattern, but can be moved to componentWillMount.
It seems to be because { this.renderCurrentAthlete() } inside render. When I call renderCurrentAthlete I'm trying to let state know who the current Athlete is by running the this.setState({ currentAthlete: currentAthleteData.Athlete }) but it causes an error. Any advise on how to handle this properly? Also any other advise on the component would be awesome too! Learning so all info is a great help :)
class MiniGame extends Component {
constructor(props){
super(props);
this.state = {
score: 0,
currentAthlete: null
}
}
gameData = [
{Athlete: "Peyton Manning", Img: "someURL"},
{Athlete: "Tony Hawk", Img: "someURL"},
{Athlete: "Tomy Brady", Img: "someURL"},
{Athlete: "Usain Bolt", Img: "someURL"}
]
renderGameButtons() {
return(
<div>
{this.gameData.map((x) => {
return(
<div key={x.Athlete}>
<button className="btn btn-outline-primary" onClick={ () => this.answerHandler(x.Athlete)}> {x.Athlete} </button>
</div>
)
})}
</div>
)
}
renderCurrentAthlete() {
const currentAthleteData = this.gameData[Math.floor(Math.random() * 4)];
//console.log(currentAthleteData);
const imgUrl = currentAthleteData.Img;
const athleteName = currentAthleteData.Athlete;
console.log(imgUrl, athleteName);
//console.log(currentAthlete);
this.setState({ currentAthlete: currentAthleteData.Athlete });
return(
<img className="card-img-top imgCard" src={imgUrl} alt="..."></img>
)
}
answerHandler(answer){
// console.log(a)
// console.log(this.state.currentAthlete)
if(answer === this.state.currentAthlete) {
this.setState({score: this.state.score + 10})
console.log(this.state.score);
}
}
render(){
return(
<div className="miniGameContainer">
<div className="card card-outline-info mb-3">
{ this.renderCurrentAthlete() }
<div className="card-block">
<p className="card-text">Pick your Answer Below</p>
{ this.renderGameButtons() }
</div>
</div>
</div>
)
}
}
Add method componentWillMount put this code to it and remove from renderCurrentAthlete. method componentWillMount will invoke before render. See more react lifecycle
componentWillMount() {
const currentAthleteData = this.gameData[Math.floor(Math.random() * 4)];
//console.log(currentAthleteData);
const imgUrl = currentAthleteData.Img;
const athleteName = currentAthleteData.Athlete;
console.log(imgUrl, athleteName);
//console.log(currentAthlete);
this.setState({ currentAthlete: currentAthleteData.Athlete });
}