Let's start by editing our application's main JavaScript file. Replace the contents of the ~/snapterest/source/app.js
file with the following code snippet:
var React = require('react'), var ReactDOM = require('react-dom'), var Application = require('./components/Application.react'), ReactDOM.render(<Application />, document.getElementById('react-application'));
There are only four lines of code in this file, and as you can guess, they provide document.getElementById('react-application')
as a deployment target for the <Application />
component and render <Application />
to the DOM. The whole user interface for our web application will be encapsulated in one React component, Application
.
Next, navigate to ~/snapterest/source/components/
and create the Application.react.js
file inside this directory. All of our React components will have their filenames ending with react.js
. This convention allows us to easily distinguish between React and non-React source JavaScript files.
Let's take a look at the contents of the Application.react.js
file:
var React = require('react'), var Stream = require('./Stream.react'), var Collection = require('./Collection.react'), var Application = React.createClass({ getInitialState: function () { return { collectionTweets: {} }; }, addTweetToCollection: function (tweet) { var collectionTweets = this.state.collectionTweets; collectionTweets[tweet.id] = tweet; this.setState({ collectionTweets: collectionTweets }); }, removeTweetFromCollection: function (tweet) { var collectionTweets = this.state.collectionTweets; delete collectionTweets[tweet.id]; this.setState({ collectionTweets: collectionTweets }); }, removeAllTweetsFromCollection: function () { this.setState({ collectionTweets: {} }); }, render: function () { return ( <div className="container-fluid"> <div className="row"> <div className="col-md-4 text-center"> <Stream onAddTweetToCollection={this.addTweetToCollection} /> </div> <div className="col-md-8"> <Collection tweets={this.state.collectionTweets} onRemoveTweetFromCollection={this.removeTweetFromCollection} onRemoveAllTweetsFromCollection={this.removeAllTweetsFromCollection} /> </div> </div> </div> ); } }); module.exports = Application;
This component has significantly more code than our app.js
file, but this code can be easily divided into three logical parts:
You will see this logical separation in most of our React components because they are wrapped into the CommonJS module pattern that allows us to easily require them with Browserify. In fact, the first and the third parts of this source file are related to how CommonJS works and have nothing to do with how React works. The purpose of using this module pattern is to break our application into modules that can be easily reused. Because the React component and CommonJS module pattern both encapsulate the code and make it portable, they naturally work great together. So, we end up encapsulating our user interface logic in a React component and then encapsulate that React component in the CommonJS module. It then can be used in any other module that wants to reuse this encapsulated React component.
In our first logical part of the Application.react.js
file, we're importing the dependency modules using the require()
function:
var React = require('react'), var Stream = require('./Stream.react'), var Collection = require('./Collection.react'),
Our Application
component will have two child components that we need to import:
Stream
component will render a stream section of our user interfaceCollection
component will render a collection section of our user interfaceWe also need to import the React
library as another module. Notice that this code is still part of the CommonJS module pattern, not React.
The second logical part of the Application.react.js
file creates the React Application
component with the following methods:
getInitialState()
addTweetToCollection()
removeTweetFromCollection()
removeAllTweetsFromCollection()
render()
Only the getInitialState()
and render()
methods are part of the React API. All the other methods are part of our application logic that this component encapsulates. We'll take a closer look at each of them right after we discuss what this component renders inside its render()
function:
render: function () { return ( <div className="container-fluid"> <div className="row"> <div className="col-md-4 text-center"> <Stream onAddTweetToCollection={this.addTweetToCollection} /> </div> <div className="col-md-8"> <Collection tweets={this.state.collectionTweets} onRemoveTweetFromCollection={this.removeTweetFromCollection} onRemoveAllTweetsFromCollection={this.removeAllTweetsFromCollection} /> </div> </div> </div> ); }
As you can see, it defines the layout of our web page using the Bootstrap framework. If you're not familiar with Bootstrap, I strongly recommend that you visit http://getbootstrap.com and read the documentation. Learning this framework will empower you to prototype user interfaces in a fast and easy way. Even if you don't know Bootstrap, it's quite easy to understand what's going on. We're dividing our web page into two columns: a smaller one and a larger one. The smaller one contains our Stream
React component and the larger one contains our Collection
component. You can imagine that our web page is divided into two unequal parts and both of them contain the React components.
This is how we're using our Stream
component:
<Stream onAddTweetToCollection={this.addTweetToCollection} />
The Stream
component has an onAddTweetToCollection
property, and our Application
component passes its own addTweetToCollection()
function as a value for this property. addTweetToCollection()
adds a tweet to a collection. It's one of the custom methods that we define in our Application
component, and we can refer to it using this
keyword.
Let's take a look at what the addTweetToCollection()
function does:
addTweetToCollection: function (tweet) { var collectionTweets = this.state.collectionTweets; collectionTweets[tweet.id] = tweet; this.setState({ collectionTweets: collectionTweets }); },
This function references CollectionTweets
that are stored in the current state, adds a new tweet to a collectionTweets
object, and updates the state by calling the setState()
function. A new tweet is passed as an argument when the addTweetToCollection()
function is called inside a Stream
component. This is an example of how a child component can update its parent component's state.
This an important mechanism in React and it works as follows:
this.props
variable.render()
function that re-renders all the child components, as necessary.This is how a child component interacts with a parent component. This interaction allows a child component to delegate the application's state management to its parent component, and it is only concerned with how to render itself. Now, when you've learned this pattern, you will be using it again and again because most of your React components should stay stateless. Only a few parent components should store and manage your application's state. This best practice allows us to logically group React components by the two different concerns that they address:
Our Application
component has a second child component, Collection
:
<Collection tweets={this.state.collectionTweets} onRemoveTweetFromCollection={this.removeTweetFromCollection} onRemoveAllTweetsFromCollection={this.removeAllTweetsFromCollection} />
This component has a number of properties:
You can see that the Collection
component's properties are only concerned about how to:
As you can guess, the onRemoveTweetFromCollection
and onRemoveAllTweetsFromCollection
functions allow the Collection
component to mutate the Application
component's state. On the other hand, the tweets
property propagates the Application
component's state to the Collection
component so that it can gain a read-only access to that state.
Can you recognize the single direction of data flow between the Application
and Collection
components? Here's how it works:
collectionTweets
data is initialized in the Application
component's getInitialState()
method.collectionTweets
data is passed to the Collection
component as the tweets
property.Collection
component calls the removeTweetFromCollection
and removeAllTweetsFromCollection
functions that update the collectionTweets
data in the Application
component, and the cycle starts again.Notice that the Collection
component cannot directly mutate the Application
component's state. The Collection
component has read-only access to that state via this.props
object, and the only way to update the parent component's state is to call the callback functions that are passed by the parent component. In the Collection
component, these callback functions are this.props.onRemoveTweetFromCollection
and this.props.onRemoveAllTweetsFromCollection
.
This simple mental model of how data flows in our React component hierarchy will help us increase the number of components we use, without increasing the complexity of how our user interface works. For example, it can have 10 levels of nested React components, as follows:
If Component G
wants to mutate the state of root Component A
, it would do it in the exact same way that Component B
, or Component F
, or any other component in this hierarchy would. However, in React, you shouldn't pass data from Component A
directly to Component G
. Instead, you should first pass it to Component B
, then to Component C
, then to Component D
, and so on until you finally reach Component G
. Component B
to Component F
will have to carry some "transit" properties that are actually only meant for Component G
. This might look like a waste of time, but this design makes it easy for us to debug our application and be able to reason out how it works. There are always strategies to optimize your application's architecture. One of them is to use Flux, which we'll discuss later in this book.
Before we finish discussing our Application
component, let's take a look at the two methods that mutate its state:
removeTweetFromCollection: function (tweet) { var collectionTweets = this.state.collectionTweets; delete collectionTweets[tweet.id]; this.setState({ collectionTweets: collectionTweets }); },
The removeTweetFromCollection()
method removes a tweet from a collection of tweets that we store in the Application
component's state. It takes the current collectionTweets
object from the component's state, deletes a tweet with a given ID from that object, and updates the component's state with an updated collectionTweets
object.
On the other hand, the removeAllTweetsFromCollection()
method removes all the tweets from the component's state:
removeAllTweetsFromCollection: function () { this.setState({ collectionTweets: {} }); },
Both of these methods are called from a child's Collection
component because that component has no other way to mutate the Application
component's state.
18.225.55.151