Props and State in React.js
React.js is the most powerful tool I have found for building UI components. It is amazing to be able to write my views in JavaScript as simple functions, then just re-render my view any time the data changes. Unlike Backbone and many other front-end frameworks, React does not wipe away and then re-render the page from scratch. React keeps track of the state of the page at the last render and when you render again, it only touches the minimum amount of HTML nodes necessary to properly represent the new data on the page.
The way this works is that you pass in data as props into your React view. If you are not familiar with how React works, let me quickly give you an example.
Creating A React View
Let's first create a simple view. I am going to create a React view called NewsList.
let NewsList = React.createClass({
render: function() {
return (
<div>
<h4>Hi! This is the news list!</h4>
</div>
);
}
});
If I wanted to take that and render it into a container on my HTML page with an ID of js-newsContainer, I would write:
React.render(<NewsList />, document.getElementById('js-newsContainer'));
That will render our simple React view.
Adding Data Using Props
So, now we have a simple view, but it does not do anything useful. I want to render a list of news, and to do that I need props. You use props to pass in data and callback methods to the React view. In my case, I want to pass in an array of news items. In most cases you would use a dynamically-generated list, but I am just going to use a static list for this example.
let news = [
{id: 1234, title: 'Swedish Chef Comes Out As Vegan.'},
{id: 3456, title: 'USS Voyager Disappears From Alpha Quadrant.'},
{id: 6789, title: 'Sonic Sunglasses To Replace Google Glass?'}
];
Now, I will pass this array in as a prop (it looks like an HTML attribute) of the React View.
React.render(<NewsList news={news} />, document.getElementById('js-newsContainer'));
We can now access those items through this.props
in our view component.
let NewsList = React.createClass({
render: function() {
let newsArr = this.props.news; // This is our news array!
return (
<div>
<h4>Hi! This is the news list!</h4>
</div>
);
}
});
Let's create a new view component for each news item.
let NewsItem = React.createClass({
render: function() {
let data = this.props.data;
return (
<li>
<div>{data.title}</div>
</li>
);
}
});
And we will add those to our NewsList component.
let NewsList = React.createClass({
render: function() {
let newsArr = this.props.news; // This is our news array!
let listItems = newsArr.map((newsObj) => {
return <NewsItem key={newsObj.id} data={newsObj} />; // I am passing in each news object as a prop called 'data'
});
return (
<ul>
{listItems}
</ul>
);
}
});
That is a basic example of how you can use props to pass in data.
Passing In Functions
Props can be used for passing in callback functions as well as data. Let's add delete functionality to each news item. First, we will pass in a deleteNews function.
let deleteNews = function(id) {
let idx = news.findIndex((obj) => (obj.id === id));
news.splice(idx, 1);
renderView();
};
let renderView = function() {
React.render(<NewsList news={news} deleteNews={deleteNews} />, document.getElementById('js-newsContainer'));
}
Now we can access that function through this.props.deleteNews
and hook it up to our components.
let NewsItem = React.createClass({
deleteClicked: function(e) { // this is where we handle the click event
e.preventDefault();
let data = this.props.data; // I grab my news item data
this.props.delete(data.id); // and pass the ID into the delete function
},
render: function() {
let data = this.props.data;
return (
<li>
<div>{data.title}<div onClick={this.deleteClicked}>X</div></div>
</li>
);
}
});
let NewsList = React.createClass({
render: function() {
let newsArr = this.props.news;
let deleteNews = this.props.deleteNews; // This is our deleteNews function, passed in as a prop from outside the component
let listItems = newsArr.map((newsObj) => {
return <NewsItem key={newsObj.id} data={newsObj} delete={deleteNews} />; // I am passing in the deleteNews function as a prop called 'delete'
});
return (
<ul>
{listItems}
</ul>
);
}
});
Adding State
Often it is not enough to just add props. Sometimes you need to keep track of state on a lower level, but do not want to clutter your real data up with state data. For my example, I want to add a hover state to make the delete button only appear when a user hovers over the list item.
To add state to a React component, you add a method called getInitialState()
. What you return from that method is your state object, and if the state changes you run this.setState([new state object])
to update the view. We will add a hovering property to our state object and set it to change when a user hovers over our list items.
let NewsItem = React.createClass({
getInitialState: function() { // what we return from here is our initial state
return {
hovering: false
};
},
onHover: function(e) {
e.preventDefault();
let state = this.state; // I grab the current state object
state.hovering = true;
this.setState(state); // I set the state with the new state object, which automatically re-renders the component with the new state data
},
offHover: function(e) {
e.preventDefault();
let state = this.state; // As above, I grab the current state object
state.hovering = false;
this.setState(state); // As above, I set the state with the new state object
},
deleteClicked: function(e) {
e.preventDefault();
let data = this.props.data;
this.props.delete(data.id);
},
render: function() {
let state = this.state; // I grab the current state data
let data = this.props.data;
return (
<li onMouseOver={this.onHover} onMouseOut={this.offHover}>
<div">
{data.title}
<div onClick={this.deleteClicked} style={(state.hovering) ? {display: 'block'} : {}}>X</div>
</div>
</li>
);
}
});
So now we are using both state and props to keep our data and our views up-to-date. If any of the news data changes, we re-render the NewsList with the new props. But if our hover state changes, each child component handles it itself.
When I first began using React, I did not know about using getInitialState()
or setState()
. Instead, I put all of my temporary user state in my actual data and fed it back down through props. It was slower than it needed to be, and it also cluttered up my real data with hover data and other information which I did not want in my real data.
Knowing how to use hover and props together has allowed me to keep my real data clean, while also keeping track of temporary UI state necessary for my JavaScript application.
Why?
Here is the big question. Why is this a big deal? What makes React so great? What is so exciting about props and state?
In traditional JavaScript apps, you would render dumb HTML to your pages. If you needed to keep track of state in your elements, you would stuff that data into the id, class, or data attributes of the HTML element. Then you would grab that data from the DOM when click or other events happened. Then, if the data changed, you would have to find and replace or add those DOM elements manually to reflect the new state of your data.
Backbone.js partially solved this, in allowing you to make smarter views which have their own data models and events. Others have improved on this model further. But React is different because all of you data is kept intact, in one place, and flows down into your view components through props. So when any data changes, you don't manually change DOM elements or update multiple data models throughout your app. Instead, you send the change up to the top level, call render, and the data flows down through your view components and React adjusts the HTML just enough to make it reflect your new data.
React is fast, React only changes the DOM when it absolutely has to, and each React component has its own data context through props and state.
To learn more, visit: https://facebook.github.io/react/