React.js Study Note (2) - Making a simple dynamic router
I am exploring ReactJS a bit further. I think the key part to understand is how data flows between compoents. The rule is “Data (props) flows from parents to children while events bubble up from children to parents. State or props change will cause compoents to refresh.”. With this in mind it is easy to create a dynamic UI. Today, I am going to create a simple UI that gets all the svgs from a static folder, and give user a list of svg files so they can choose to display selected svg. This is like a very simple file server.
Create the backend Service
First, we need to create a server. You can choose the technology you are mostly comfortable with. Here I am using nodeJS to server files and static contents.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25var http = require('http');
var url = require('url');
var fs = require('fs');
var server = http.createServer(function(req, res){
res.writeHead(200, {'Content-Type':'text/plain', 'Access-Control-Allow-Origin':'http://localhost:8000'});
var basePath = 'svgs/';
var query = url.parse(req.url, true).query;
var name = query.name;
if(name === 'all'){
fs.readdir(basePath, function(err, files){
res.end(JSON.stringify(files));
});
} else {
var path = basePath + name;
fs.readFile(path, 'utf8',function(err, content){
res.end(content);
});
}
});
server.listen(1337);
console.log('Server is running');Several things to notice here, 1) I set ‘Access-Control-Allow-Origin’ to ‘http://localhost:8000' because later, I need to serve react jsx file with anther static file http server. By the CORS rule of http protocol, our server needs to return this in the http response header to make itself accessible from another http server. This is not sercure but it makes the demonstration easier. 2) I used query string, using a restful url path is actually more preferable.
Now run the server as
node main.js
.Create React jsx file
First, I am creating different components. LeftListBox is used to display a list of files in the folder, RightSvgBox shows the svg content when an item from left is chosen. Jquery ajax is used to retrieve data from backend server.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122(function ($) {
var NamesList = React.createClass({
handleClick: function (name, index) {
this.setState({selectedIndex:index});
this.props.onUpdate(index);
},
render: function () {
var namesList = this.props.names.map(function (name, index) {
return (
<li>
<a onClick={this.handleClick.bind(this, name, index)}>{name}</a>
</li>
);
}.bind(this));
return (
<ul className="namesList">
{namesList}
</ul>
);
}
});
var LeftListBox = React.createClass({
render: function () {
return (
<div className="leftListBox">
<NamesList names={this.props.names} onUpdate={this.props.onUpdate}></NamesList>
</div>
);
}
});
var RightSvgBox = React.createClass({
getInitialState: function(){
return {image:"<div>The image is empty</div>"};
},
componentDidMount: function(){
this.setState({image:"<div>The image is empty</div>"})
},
componentWillReceiveProps: function () {
var selectedName;
if(this.props.names.length == 0){
selectedName = "";
} else {
selectedName = this.props.names[this.props.selectedIndex];
}
var fullUrl = this.props.baseurl + "?name=" + selectedName;
$.ajax({
url: fullUrl,
cache: true,
success: function (svg) {
if(svg.length === 0){
this.setState({image:"<div>Choose one from the left...</div>"})
} else {
var re = /<svg/;
svg = svg.replace(re, '<svg viewBox="0 0 1280 768"');
this.setState({image: svg})
}
}.bind(this),
error: function (xhr, status, err) {
console.error(fullUrl, status, err.toString());
}
});
},
render: function () {
return (
<div className="rightSvgBox" dangerouslySetInnerHTML={{__html: this.state.image}}>
</div>
);
},
});
var DisplayPage = React.createClass({
getInitialState: function(){
return {names:[], selectedIndex:0};
},
componentDidMount: function () {
$.ajax({
url: this.props.baseurl + '?name=all',
dataType:'json',
cache: true,
success: function (names) {
this.setState({ names: names });
}.bind(this),
error: function (xhr, status, err) {
console.error(xhr);
}.bind(this),
});
},
onUpdate : function(selectedIndex){
this.setState({selectedIndex:selectedIndex});
},
render: function() {
return (
<div className="row">
<div className="col-md-3">
List of Available Files:
<LeftListBox names={this.state.names} onUpdate={this.onUpdate}></LeftListBox>
</div>
<div className="col-md-9">
<RightSvgBox names={this.state.names} baseurl={this.props.baseurl} selectedIndex={this.state.selectedIndex}></RightSvgBox>
</div>
</div>
);
}
});
var baseurl = 'http://localhost:1337/check';
var url = baseurl + '?name=all';
React.render(
<DisplayPage baseurl={baseurl}>Sample</DisplayPage>,
document.getElementById('__reactApp')
);
})(jQuery);Explanation of the code:
First I created a compoent
DisplayPage
. I am intending to split the view into two columns, left column will be a list of clickable options (LeftListBox), right column will the actual svg (RightSvgBox).First, we need to use ajax to retrieve data from backend, according the document of componentDidMount. We put our ajax into this function because we want to retrieve the data before DOM is rendered.
The retrieved data is set into state (Remeber to always use this.setState!). I am then passing names through props from parent(DisplayPage) to children (LeftListBox and RightSvgBox). Notice the names={this.state.names} attribute in the html.
Children take data through this.props.names. Notice we are also passing an OnUpdate callback from parent to children. This is because we want to bubble up some information from children to parent. Upon getting new data, this callback is called, and parent state is mutated. All the children will react to to modified state and go through the page cycle again.
componentWillReceiveProps is pretty handy in my application because when user click on the link, i need to send an ajax to backend to get svg. After successfully getting data, I then modified state and render the page. This method is not called during initial rendering and changing state within this function does not trigger addition render. So I can safely set the page state in the ajax sucess callback. If we use other method like compoentWillUpdate, we run into infinite update loop.