Delay array map iteration in React - javascript

I have this array, that I want to iterate. I need to delay it by a couple of seconds before the next.
{this.props.things.map((thing, index) => {
return (
<div key={index}>{thing.content}</div>
// Delay 1 second here
)
})}
The initial state of this array is always more than one. For UI purposes I want them to load in one by one in to the DOM.

The render function of react is synchronous. Also javascript map is synchronous. So using timers is not the right solution here.
You can however, in your component state, keep track of items that have been rendered and update that state using javascript timers:
For an example implementation check out this fiddle:
React.createClass({
getInitialState() {
return {
renderedThings: [],
itemsRendered: 0
}
},
render() {
// Render only the items in the renderedThings array
return (
<div>{
this.state.renderedThings.map((thing, index) => (
<div key={index}>{thing.content}</div>
))
}</div>
)
},
componentDidMount() {
this.scheduleNextUpdate()
},
scheduleNextUpdate() {
this.timer = setTimeout(this.updateRenderedThings, 1000)
},
updateRenderedThings() {
const itemsRendered = this.state.itemsRendered
const updatedState = {
renderedThings: this.state.renderedThings.concat(this.props.things[this.state.itemsRendered]),
itemsRendered: itemsRendered+1
}
this.setState(updatedState)
if (updatedState.itemsRendered < this.props.things.length) {
this.scheduleNextUpdate()
}
},
componentWillUnmount() {
clearTimeout(this.timer)
}
})

Related

I want to put a delay per map iteration [duplicate]

I have this array, that I want to iterate. I need to delay it by a couple of seconds before the next.
{this.props.things.map((thing, index) => {
return (
<div key={index}>{thing.content}</div>
// Delay 1 second here
)
})}
The initial state of this array is always more than one. For UI purposes I want them to load in one by one in to the DOM.
The render function of react is synchronous. Also javascript map is synchronous. So using timers is not the right solution here.
You can however, in your component state, keep track of items that have been rendered and update that state using javascript timers:
For an example implementation check out this fiddle:
React.createClass({
getInitialState() {
return {
renderedThings: [],
itemsRendered: 0
}
},
render() {
// Render only the items in the renderedThings array
return (
<div>{
this.state.renderedThings.map((thing, index) => (
<div key={index}>{thing.content}</div>
))
}</div>
)
},
componentDidMount() {
this.scheduleNextUpdate()
},
scheduleNextUpdate() {
this.timer = setTimeout(this.updateRenderedThings, 1000)
},
updateRenderedThings() {
const itemsRendered = this.state.itemsRendered
const updatedState = {
renderedThings: this.state.renderedThings.concat(this.props.things[this.state.itemsRendered]),
itemsRendered: itemsRendered+1
}
this.setState(updatedState)
if (updatedState.itemsRendered < this.props.things.length) {
this.scheduleNextUpdate()
}
},
componentWillUnmount() {
clearTimeout(this.timer)
}
})

Component Doesn't Update once the Parent component's state is updated

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>

React - setTimeout triggers style change for wrong components

reproduced here: https://jsfiddle.net/69z2wepo/204131/
A parent component has two 'notifications' that it renders with different 'decay' rates.
class Page extends React.Component {
constructor(props) {
super(props);
this.state = {
notifications: [
{ message: "I am the first component", code: 1, decay: 2000 },
{ message: "I am the second component", code: 2, decay: 5000 }
]
}
this.dismissNotification = this.dismissNotification.bind(this)
}
dismissNotification(code) {
this.setState({ notifications: this.state.notifications.filter(
n => n.code != code
)})
}
render() {
return (
<div>
{
this.state.notifications.map( (n, idx) => {
return (
<Notification
key={idx}
code={n.code}
decay={n.decay}
dismiss={this.dismissNotification}
>
{n.message}
</Notification>
)
})
}
</div>
)
}
}
The components set their own timeOut which will cause an animation and then send a message for them to be dismissed.
class Notification extends React.Component {
constructor(props) {
super(props)
this.state = {
style: { opacity: 1 }
}
this.makeRedFunction = this.makeRedFunction.bind(this)
}
componentDidMount = () => {
let timeout = parseInt(this.props.decay) || 2000
setTimeout(() => {
this.makeRedFunction();
setTimeout(() => {
this.dismiss();
}, 125)
}, timeout)
}
fadeOutFunction = () => {
let opacity = Math.floor(this.state.style.opacity * 10)
if (opacity > 0) {
opacity -= 1
setTimeout( () => { this.fadeOutFunction() }, 10)
}
let newState = Object.assign({}, this.state.style)
newState.opacity = opacity / 10
this.setState({ style: newState })
}
makeRedFunction = () => {
this.setState({ style: {color: 'red'} })
}
dismiss = () => {
this.props.dismiss(this.props.code)
}
render () {
return(
<div style={this.state.style}>{this.props.children}</div>
)
}
}
ReactDOM.render(
<Page/>,
document.getElementById('container')
);
Unforunately, the style seems to change for both notifications when the dismiss function has been called for only one of them.
In general there is strange behavior with the mounting lifecycle of the components with this approach.
tl;dr: Don't use array indexes as keys if elements in the list have state. Use something that is unique for each data point and does not depend on its position in the array. In your case that would be key={n.code}.
This is related to how React reconciles the component tree and is a good example for why using array index as keys doesn't always produce the expected outcome.
When you are mutating a list of elements, the key helps React to figure out which nodes it should reuse. In your case are going from
<Notification />
<Notification />
to
<Notification />
But how should React know whether to delete the first or second <Notification /> node? It does that by using keys. Assume we have
<Notification key="a">Foo</Notification>
<Notification key="b">Bar</Notification>
Now if it gets either
<Notification key="a">...</Notification>
or
<Notification key="b">...</Notification>
in the next render cycle it knows to remove the <Notification /> with key "b" (or "a").
However, your problem is that you base the key on the position of the data in the array. So on the first render you pass
<Notification key="0">First</Notification>
<Notification key="1">Second</Notification>
Then you are removing the first notification from the list, which changes the position of the second notification in the array, so React gets
<Notification key="0">Second</Notification>
which means
remove the element with key 1 and update the element with key 0 to show "Second"
But the element with key="0" already had its style changed to red text, so you see the text from the second notification in red.
Have a look at the documentation for more information.

React comments component - Target newly added comment component in list and run function on only this one

I am looking to create a comment edit timer which triggers when a new comment is added. It will show an 'Edit' button for 60 seconds before the button is removed (similar to what The Verge do with their comments).
I have a Comments component which makes an API call after componentDidMount() and renders a list of Comment components (by adding comment objects into my comment: [] state. I also have a CommentForm component which allows users to submit a new comment.
When a comment is successfully submitted the API call returns a complete comment object which I then prepend to the existing comment state array. I also update my newCommentId state with the new comment id and set my startEditTimer boolean state to true.
postCommentSuccess = (res) => {
const newArray = this.state.comments.slice(0);
newArray.splice(0, 0, res.data);
this.setState({
comments: newArray,
newCommentId: res.data.id,
startEditTimer: true,
});
}
I render the list of comments like this...
render() {
if (this.state.comments.length) {
commentsList = (this.state.comments.map((comment) => {
const { id } = comment;
return (
<Comment
key={id}
id={id}
newCommentId={this.state.newCommentId}
startEditTimer={this.state.startEditTimer}
/>
);
}));
}
return (
<ul className="comments-list">
{commentsList}
</ul>
);
}
In my Comment component I am checking to see if the startEditTimer prop is set to true and then running the startEditTimer() function.
componentWillReceiveProps(nextProps) {
if (nextProps.startEditTimer === true) {
this.startEditTimer();
}
}
startEditTimer = () => {
this.setState({ editTimer: 60 });
setInterval(this.countdown, 1000);
}
countdown = () => {
this.setState({ editTimer: this.state.editTimer -= 1 });
}
In my return function I am then showing/hiding the edit button like so:
render() {
return (
<li className="comment">
{this.props.id === this.props.newCommentId &&
this.state.editTimer > 0 &&
<button onClick={this.editReply} className="edit-btn">Edit ({this.state.editTimer})</button>
}
</li>
);
}
This works to an extent, the edit button does show on a new comment when it is posted, but the countdown timer does not last 60 seconds, instead it seems to be reduced by one every 0.5 seconds or so. I believe this could be because the startEditTimer() function is running multiple times when a new comment is added instead of just once so I believe I need a way of only running the function on the newly added comment.
Another approach is to just pass the created time to the Comment component. Then, in the Comment component you add a setInterval-function that checks every second if the time passed since the create time is greater than 60 seconds. Could look something like this:
// Commentlist component
render() {
if (this.state.comments.length) {
commentsList = (this.state.comments.map((comment) => {
const { id, createdTime } = comment;
return (
<Comment
key={id}
id={id}
createdTime={createdTime}
newCommentId={this.state.newCommentId}
/>
);
}));
}
return (
<ul className="comments-list">
{commentsList}
</ul>
);
}
Then in the Comment component:
// Comment component
componentDidMount() {
this.intervalChecker = setInterval(() => {
if((Date.now() - this.props.createdDate)/1000 >= 60) {
this.setState({ showEditButton: false})
clearInterval(this.intervalChecker)
}
}, 1000)
}
render() {
return (
<li className="comment">
{this.state.showEditButton &&
<button onClick={this.editReply} className="edit-btn">Edit ({this.state.editTimer})</button>
}
</li>
);
}
Also, see the following gist: https://jsbin.com/luhehitune/3/edit?js,output

Trouble Getting Component to Render in setTimeout

Not sure what I'm doing wrong but my component wrapped in setTimeout is not being rendered to the DOM:
const ContentMain = Component({
getInitialState() {
return {rendered: false};
},
componentDidMount() {
this.setState({rendered: true});
},
render(){
var company = this.props.company;
return (
<div id="ft-content">
{this.state.rendered && setTimeout(() => <Content company={company}/>,3000)}
</div>
)
}
})
I'd bet this isn't working because the render method needs all of its input to be consumed at the same time and it can't render other components in retrospect, there's a certain flow to React. I'd suggest to separate the timeout from render method anyway for logic's sake, and do it in componentDidMount like this:
const ContentMain = Component({
getInitialState() {
return {rendered: false};
},
componentDidMount() {
setTimeout(() => {
this.setState({rendered: true});
}, 3000);
},
render(){
if (!this.state.rendered) {
return null;
}
var company = this.props.company;
return (
<div id="ft-content">
<Content company={company}/>
</div>
)
}
})
Changing the state triggers the render method.
On a side note - even if your original approach worked, you'd see the component flicker for 3 seconds every time it got rendered after the initial load. Guessing you wouldn't want that :)

Categories