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 have a component which you can toggle on/off by clicking on it:
clickHandler = () => {
this.setState({active: !this.state.active})
this.props.getSelection(this.state.active)
}
render() {
const { key, children } = this.props;
return (
<button
key={key}
style={{...style.box, background: this.state.active ? 'green' : ''}}
onClick={() => this.clickHandler()}
>
{children}
</button>
);
}
In the parent component, I pass down a method in order to try and get the value of the selected element pushed into an array, like so:
getSelection = (val) => {
const arr = []
arr.push(val);
console.log(arr, 'arr');
}
My problem is that it only ever adds one element to the array, so the array length is always 1 (even if more than one item has been clicked).
Current result (after you've clicked all three)
console.log(arr, 'arr') // ["Birthday"] "arr"
Expected result (after you've clicked all three)
console.log(arr, 'arr') // ["Birthday", "Christmas", "School achievement"] "arr"
Link to Codepen
Any ideas?
Two things:
setState is async, so on the next line you might or might not get the latest value, so I recommend changing
clickHandler = () => {
this.setState({active: !this.state.active})
this.props.getSelection(this.state.active)
}
to
clickHandler = () => {
this.setState({active: !this.state.active}, () => {
this.props.getSelection(this.state.active)
})
}
The second argument to the setState is a callback function that will be executed right after the setState is done.
The second thing, on getSelection you are defining a new array each time you get there, so it won't have the values from the previous run. You should store it somewhere.
There are 2 problems here:
arr is local variable. It doesn't keep the previous onClick result.
setState is an asynchronous event. According to documentation:
setState() does not always immediately update the component.
setState((state, props) => {}, () => { /*callback */}) should be used.
class Box extends React.Component {
state = {
active: false
};
clickHandler = () => {
this.setState(
state => ({ active: !state.active }),
() => {
this.props.getSelection(this.state.active);
}
);
};
render() {
const { children } = this.props;
return (
<button
style={{ ...style.box, background: this.state.active ? "green" : "" }}
onClick={this.clickHandler}
>
{children}
</button>
);
}
}
Minor note:
The key value isn't in the child component's this.props, so you don't have to pass it, but it will not affect the outcome.
In App component, let's create an array in class level for the sake of display:
class App extends React.Component {
state = {
needsOptions: ["Birthday", "Christmas", "School achievement"]
};
arr = [];
getSelection = val => {
this.arr.push(val);
console.log(this.arr);
};
}
CodePen here
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.
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
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 :)
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)
}
})