I'm trying to understand how to send new variable if I use state.
It's example from React tutorial:
var Timer = React.createClass({
getInitialState: function() {
return {secondsElapsed: 0};
},
tick: function() {
this.setState({secondsElapsed: this.state.secondsElapsed + 1});
},
componentDidMount: function() {
this.interval = setInterval(this.tick, 1000);
},
componentWillUnmount: function() {
clearInterval(this.interval);
},
render: function() {
return (
<div>Seconds Elapsed: {this.state.secondsElapsed}</div>
);
}
});
React.render(<Timer />, mountNode);
It works. But If I want to send secondsElapsed what should I do?
<Timer sec={this.props.sec}>and:
getInitialState: function() {
return {secondsElapsed: this.props.sec};
}
It doesn't work.
JSFIDDLE - I should start timer form 10000 sec.
The code below works as expected. Other than making sure there's a default value always for the initialTime property, you can initialize the secondsElapsed state within getInitialState. While doing this could be considered an anti-pattern, in this case it's not as it's just doing initialization of internal state.
Also, you'll need to implement componentWillReceiveProps. This function is called (after the first time) when ever the props to the component have been updated. In this case, the initialTime property value was initially 10, and then changed to 10000. So, the nextProps argument will contain an object with the property initialTime set to 10000.
var Timer = React.createClass({
getInitialState: function() {
return { secondsElapsed: this.props.initialTime || 0 };
},
tick: function() {
this.setState({secondsElapsed: this.state.secondsElapsed + 1});
},
componentWillReceiveProps: function(nextProps) {
this.setState({ secondsElapsed: nextProps.initialTime || 0 });
},
componentDidMount: function() {
this.interval = setInterval(this.tick, 1000);
},
componentWillUnmount: function() {
clearInterval(this.interval);
},
render: function() {
return (
<div>Seconds Elapsed: {this.state.secondsElapsed}</div>
);
}
});
If you might be passing a string into initialTime as shown above, make sure that you use parseInt on the value:
return { secondsElapsed: parseInt(this.props.initialTime || '0') };
Read more about the component lifecycle here.
Check out componentWillMount (http://facebook.github.io/react/docs/component-specs.html)
This if you call setState within componentWillMount you are guaranteed it will be updated before it hits render.
Related
I'm trying to implement timer that counts the next event from the JSON API. Unfortunately the API does not support filter so I have to process it on the component
here's the code:
constructor(props) {
super(props);
this.scrollViewRef = React.createRef();
this.state = {
agendaVisible: false,
scrollOffset: null,
markedDates: {},
dateString: Date(),
agendaItems: {},
nextEvent: {
start: 99999999999,
name: "",
},
isProcessing: true,
};
this.closeModal = this.closeModal.bind(this);
this.openModal = this.openModal.bind(this);
}
setMarkedDates = () => {
const { events } = this.props;
if (events !== undefined) {
Object.keys(events).map((key) => {
let now = Date.now();
let difference = events[key].start * 1000 - now;
let a = new Date(events[key].start * 1000);
let date = `${a.getUTCFullYear()}-${this.str_pad(
a.getUTCMonth() + 1
)}-${this.str_pad(a.getUTCDate())}`;
if (difference > 0 && difference < this.state.nextEvent.start) {
this.setState({
nextEvent: {
start: events[key].start * 1000,
name: events[key].name,
},
});
console.log("Goes in here: " + events[key].start);
}
}
// Set Marked dates on calendar and agenda Items
this.setState(({ markedDates, agendaItems }) => ({
markedDates: {
...markedDates,
[date]: {
marked: true,
selectedColor: "blue",
activeOpacity: 0,
dotColor: "blue",
},
},
agendaItems: {
...agendaItems,
[date]: [
{
name: events[key].name,
height: 100,
},
],
},
}));
});
this.setState({ isFetching: false });
}
};
Then I passed in the nextEvent to my EventTimer component in the render
{!this.state.isFetching && (
<EventTimer
nextEvent={this.state.nextEvent}
nextEventStart={this.state.nextEventStart}
/>
)}
Now the problem is, whenever I called {this.props.nextEvent.start} in the render, it works, however I need to set it up the state of eventTimer for static endTime to calculate the difference every interval, I put the code on componentWillReceiveProps but I never get the updated props? it still stays at 9999999999, what happened?
If it doesn't receive the props then which part of the lifecycle received the props? Because it works when I tried to render it. I'm not sure where should I update the state from the props anymore.
Please help, Thanks!
componentWillReceiveProps(nextProps) {
this.setState({
endTime: Date.now() + this.props.nextEvent.start,
});
this.resetTimer();
}
resetTimer = () => {
if (this.interval) clearInterval(this.interval);
this.interval = setInterval(
this.setTimeRemaining,
this.timeRemaining % 1000
);
};
You are referring to the initial props in the componentWillReciveProps.
this.setState({
// this.props refers to the current props.
// endTime: Date.now() + this.props.nextEvent.start,
endTime: Date.now() + nextProps.nextEvent.start,
});
While that should fix the bug you are facing. You should try to not use this lifecycle method and instead depend on a calculated value from the parent component.
The componentWillReciveProps will be deprecated in the next major version. You can read a bit more about this in the react docs.
I have got problem with my react component which renders form. Basically, when I enter countdown page, the form just doesn't work (by that I mean it doesnt act at all, I write for example 123 which is 2 min and 3 seconds and nothing happens, just nothing). But, for example, if I go on to main page and back to countdown page, it works. I have noticed that when entering this page the first time, componentWillMount works, but componentDidMount doesn't (it won't console.log the message).
Link to heroku: http://fathomless-lowlands-79063.herokuapp.com/?#/countdown?_k=mj1on6
CountdownForm.jsx
var React = require('react');
var CountdownForm = React.createClass({
onSubmit: function (e) {
e.preventDefault();
var strSeconds = this.refs.seconds.value;
if (strSeconds.match(/^[0-9]*$/)){
this.refs.seconds.value = '';
this.props.onSetCountdown(parseInt(strSeconds, 10));
}
},
render: function () {
return(
<div>
<form ref="form" onSubmit={this.onSubmit} className="countdown-form">
<input type="text" placeholder="Enter time in seconds" ref="seconds" />
<button className="button expanded">Start
</button>
</form>
</div>
);
}
});
module.exports = CountdownForm;
Countdown.jsx
var React = require('react');
var Clock = require('Clock');
var CountdownForm = require('CountdownForm');
var Controls = require('Controls');
var Countdown = React.createClass({
getInitialState: function () {
return {
count: 0,
countdownStatus: 'stopped'
};
},
componentDidUpdate: function (prevProps, prevState) {
if (this.state.countdownStatus !== prevState.countdownStatus)
{
switch (this.state.countdownStatus){
case 'started':
this.startTimer();
break;
case 'stopped':
this.setState({count: 0})
case 'paused':
clearInterval(this.timer)
this.timer = undefined;
break;
}
}
},
componentDidMount: function() {
console.log("componentDidMount");
},
componentWillMount: function () {
console.log("componentWillMount");
},
componentWillUnmount: function () {
console.log('componentDidUnmount');
},
startTimer: function () {
this.timer = setInterval(() => {
var newCount = this.state.count - 1;
this.setState({
count: newCount >= 0 ? newCount : 0
});
}, 1000);
},
handleSetCountdown: function (seconds){
this.setState({
count: seconds,
countdownStatus: 'started'
});
},
handleStatusChange: function (newStatus) {
this.setState({
countdownStatus: newStatus
});
},
render: function () {
var {count, countdownStatus} = this.state;
var renderControlArea = () => {
if (countdownStatus !== 'stopped') {
return <Controls countdownStatus={countdownStatus} onStatusChange={this.handleStatusChange} />
} else {
return <CountdownForm onSetCountdown={this.handleSetCountdown} />
}
};
return(
<div>
<Clock totalSeconds={count} />
{renderControlArea()}
</div>
);
}
});
module.exports = Countdown;
I have solved the problem. Main issue was error: "Uncaught Error: Stateless function components cannot have refs.
at invariant" . The problem was with stateless components, so that's why I instead of using arrow functions in Main.jsx and Nav.jsx, I used the React.createClass({}).
I am trying to pass data received in one component of a React application. On success I am taking the received data and setting the state and then trying to pass that data as a property of the next component. Once inside the second component I need to access the passed data via this.state so I can change the state in that component later. I seem to be encountering an issue with the DOM rendering before the data is received from the service. I have tried passing an already loaded array of values in place of this.state.data in <List data={this.state.data}/> and it seems to execute fine. How can assure that I have received the data from the service before rendering the DOM so that the data is passed all the way down to each component.
EDIT: added full implementation of the List element so explain the use of this.state
This is basically what I am trying to do:
var Box = React.createClass({
getInitialState: function() {
return {data: []};
},
loadTodosFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'GET',
cache: false,
success: function(dataResponse) {
this.setState({data: dataResponse});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
componentDidMount: function() {
this.loadFromServer();
},
render: function() {
return (<List data={this.state.data}/>);
}
});
var List = React.createClass({
getInitialState: function() {
return {data: this.props.data};
},
dragStart: function(e) {
this.dragged = e.currentTarget;
e.dataTransfer.effectAllowed = 'move';
// Firefox requires dataTransfer data to be set
e.dataTransfer.setData("text/html", e.currentTarget);
},
dragEnd: function(e) {
this.dragged.style.display = "block";
this.dragged.parentNode.removeChild(placeholder);
// Update data
var data = this.state.data;
var from = Number(this.dragged.dataset.id);
var to = Number(this.over.dataset.id);
if(from < to) to--;
if(this.nodePlacement == "after") to++;
data.splice(to, 0, data.splice(from, 1)[0]);
this.setState({data: data});
},
dragOver: function(e) {
e.preventDefault();
this.dragged.style.display = "none";
if(e.target.className == "placeholder") return;
this.over = e.target;
// Inside the dragOver method
var relY = e.clientY - this.over.offsetTop;
var height = this.over.offsetHeight / 2;
var parent = e.target.parentNode;
if(relY > height) {
this.nodePlacement = "after";
parent.insertBefore(placeholder, e.target.nextElementSibling);
}
else if(relY < height) {
this.nodePlacement = "before"
parent.insertBefore(placeholder, e.target);
}
},
render: function() {
var results = this.state.data;
return (
<ul>
{
results.map(function(result, i) {
return (
<li key={i}>{result}</li>
)
})
}
</ul>
);
}
});
ReactDOM.render(
<Box url="/api/comments"/>, document.getElementById('content')
);
The reason why your data load subsequent to component load is not rendering the data is because of this line in your List.render function:
var results = this.state.data;
Essentially, you have made a copy of your original props and assigned them to the state in the List component using the getInitialState method. And after that your state and props are delinked. Which means that if the props.data changes on the List component, the state doesn't know about it, so therefore nothing gets re-rendered.
So, instead of using state to initialize the results variable, use props.
var results = this.props.data
Here's how it would look like:
var List = React.createClass({
render: function() {
var results = this.props.data;
return (
<ul>
{
results.map(function(result, i) {
return (
<li key={i}>{result}</li>
)
})
}
</ul>
);
}
});
Now anytime the data changes, props get updated and eventually the results get re-rendered.
Updated to address the comments from the OP:
If you want to update the state of the list but want to be notified every time the props at the parent change, then you want to use the method componentWillReceiveProps so that when the data is obtained the child List is notified. And in this method you can set the new state:
componentWillReceiveProps: function(newProps) {
this.setState({data: this.props.data});
}
Once you do this, react will re-render the list for you.
Another update: To illustrate how this works I have put together an example here.
And here's the JS code for this:
let todos = ["Run","Swim","Skate"];
class MyList extends React.Component{
componentWillMount() {
console.log("Props are: ", this.props);
this.setState({list: this.props.items});
}
componentWillReceiveProps(newProps) {
console.log("Received Props are: ", newProps);
this.setState({list: newProps.items});
}
render() {
return (<ul>
{this.state.list.map((todo) => <li>{todo}</li>)}
</ul>);
}
}
class App extends React.Component{
constructor() {
super();
console.log("State is: ", this.state);
}
componentWillMount() {
this.setState({items: ["Fly"]});
}
componentDidMount() {
setTimeout(function(){
console.log("After 2 secs");
this.setState({items: todos});
}.bind(this), 2000);
}
render() {
return (<MyList items={this.state.items}/>);
}
}
ReactDOM.render(<App/>, document.getElementById("app"));
I'm building a sort of clock with React that has an option to increment or decrement a number (25 as default) in one component, and in another component it updates the timer (25:00 since we start at 25) to whatever the number is incremented or decremented to.
I have two components (Session and Clock) successfully performing their own actions, however I'm stumped as to how I can get the counter (Session component) to update the state of the timer in the Clock component. More specifically, I've been toying with this.props.minutes to no avail.
Question: How can I go about sharing the this.state.minutes property among components? Thank you in advance. I'm still a total beginner at React.
Session:
const Session = React.createClass({
getInitialState: function() {
return {
minutes: 25,
seconds: 0
};
},
increment: function() {
this.setState({ minutes: this.state.minutes + 1 });
},
decrement: function() {
this.setState({ minutes: this.state.minutes - 1 });
},
timeToString: function(time) {
return time + ':00';
},
render: function() {
return (
<section>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
{this.state.minutes}
<Clock/>
</section>
);
}
});
module.exports = Session;
Clock:
const Clock = React.createClass({
getInitialState: function() {
return { currentCount: 10 };
},
startTimer: function() {
var intervalId = setInterval(this.timer, 1000);
this.setState({ intervalId: intervalId });
},
pauseTimer: function() {
clearInterval(this.state.intervalId);
this.setState({ intervalId: this.state.currentCount });
},
timer: function() {
var newCount = this.state.currentCount - 1;
if (newCount >= 0) {
this.setState({ currentCount: newCount });
} else {
clearInterval(this.state.intervalId);
}
},
render: function() {
return (
<section>
<button onClick={this.startTimer}>Start</button>
<button onClick={this.pauseTimer}>Pause</button>
{this.state.currentCount}
</section>
);
}
});
module.exports = Clock;
You need to pass in the state from Session to Clock like so:
<Clock time={this.state.minutes} /> in your Session component
Then the 'state' is now available to your Clock component as this.props.time
or whatever you call it in the above code.
The moral of the story is that state passed down to from a parent component to a child component is done so using props
Relevant Docs:
https://facebook.github.io/react/docs/multiple-components.html
Edit: another key link in the docs:
https://facebook.github.io/react/tips/communicate-between-components.html
I don't know if I worded this right, so bear with me. Basically, I have a component that is a functioning counter (increments or decrements). The other component is a timer that counts down from (by default) 25 to 0.
Previously, I had the timer just set to the value of 25, but I am trying to have the timer change as the value of the counter changes, and when the use presses the "start" button, the timer will count down from whatever number was set by the counter.
I can get the components working individually, but not together.
I've tried setting this.state.currentCount to the value of this.props.time, and then changing the value of this.state.currentCount, but no luck. Either the timer doesn't budge or it doesn't reflect the value of the counter.
Not sure if I should be using componentWillReceiveProps instead.
Any help would be greatly appreciated. There's a screenshot at the bottom if that helps at all.
Session Component:
const Session = React.createClass({
getInitialState: function() {
return {
minutes: 25,
seconds: 0
};
},
increment: function() {
this.setState({ minutes: this.state.minutes + 1 });
},
decrement: function() {
this.setState({ minutes: this.state.minutes - 1 });
},
timeToString: function(time) {
return time + ':00';
},
render: function() {
return (
<section>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
{this.state.minutes}
<Clock time={this.state.minutes}/>
</section>
);
}
});
module.exports = Session;
Clock Component:
const Clock = React.createClass({
getInitialState: function() {
return { currentCount: this.props.time };
},
startTimer: function() {
var intervalId = setInterval(this.timer, 1000);
this.setState({ intervalId: intervalId });
},
pauseTimer: function() {
clearInterval(this.state.intervalId);
this.setState({ intervalId: this.props.time });
},
timer: function() {
var newCount = this.state.currentCount - 1;
if (newCount >= 0) {
this.setState({ currentCount: newCount });
} else {
clearInterval(this.state.intervalId);
}
},
render: function() {
return (
<section>
<button onClick={this.startTimer}>Start</button>
<button onClick={this.pauseTimer}>Pause</button>
{this.props.time}
<br></br>
{this.state.currentCount}
</section>
);
}
});
module.exports = Clock;
getInitialState only runs when the component is first initialized so on next
updates from the parent component it won't run that function. You are correct
in that you want to use one of the lifecycle events and in this case
componentWillReceiveProps sounds like the most appropriate because you can
setState there and you don't need to wait for the component to render (otherwise
you would use componentDidUpdate).
I haven't checked this code but I think it should work with this addition:
const Clock = React.createClass({
...
componentWillReceiveProps: function(nextProps) {
// Perhaps pause timer here as well?
this.setState({
currentCount: nextProps.time
})
},
...
});
because your timer depends on Start button. it would be good if you set state of currentCount in startTimer method.
startTimer: function() {
if(this.state.intervalId)
clearInterval(this.state.intervalId); //clear the running interval
this.setState({ currentCount: this.props.time }); // reset currentcount
var intervalId = setInterval(this.timer, 1000);
this.setState({ intervalId: intervalId });
},