Skip to content
This repository has been archived by the owner on Feb 1, 2022. It is now read-only.

Sample application: dynamic client-side app using express-react-views #17

Merged
merged 8 commits into from Jan 8, 2015
1 change: 1 addition & 0 deletions examples/dynamic/.gitignore
@@ -0,0 +1 @@
public/main.js
30 changes: 30 additions & 0 deletions examples/dynamic/app.js
@@ -0,0 +1,30 @@
var express = require('express');
var reactViews = require('express-react-views');

var app = express();

app.set('view engine', 'js');
app.engine('js', reactViews.createEngine({
jsx: {
extension: '.js'
}
}));

app.use(express.static(__dirname + '/public'));

app.get('/', function (req, res) {
var initialState = {
items: [
'document your code',
'drop the kids off at the pool',
'</script><script>alert(666)</script>'
],
text: ''
};
res.render('Html', { data: initialState });
});

var port = process.env.PORT || 3000;
app.listen(port, function () {
console.log('Dynamic react example listening on port ' + port);
});
18 changes: 18 additions & 0 deletions examples/dynamic/package.json
@@ -0,0 +1,18 @@
{
"name": "dynamic-views",
"version": "0.1.0",
"description": "Example of creating a dynamic app using express-react-views",
"author": "Chris Johnson <tenorviol@yahoo.com>",
"private": true,
"scripts": {
"preinstall": "npm install ../../",
"start": "browserify -t reactify --standalone main views/main.js -o public/main.js; node app.js"
},
"dependencies": {
"browserify": "^8.0.3",
"express": "^4.10.6",
"react": "^0.12.2",
"react-tools": "^0.12.2",
"reactify": "^0.17.1"
}
}
Binary file added examples/dynamic/public/favicon.ico
Binary file not shown.
36 changes: 36 additions & 0 deletions examples/dynamic/readme.md
@@ -0,0 +1,36 @@
Dynamic react views example
===========================

This example is the todo list borrowed from the
[react.js main page](http://facebook.github.io/react/).
We render the application server-side using express-react-views.
An initial set of items has been added
to illustrate populating data from the server.


run it
------

npm install
npm start


How it works
------------

1. Separate the page into two templates,
a [static container component](views/Html.jsx)
and a [dynamic inner component](views/Content.jsx).

2. Use express-react-views to render and serve the container.
Server-side data can be sent via view options.

3. Make your views available client-side as javascript.
Here I created a [main](views/main.jsx) function for bootstrapping
and packaged it up using [browserify](http://browserify.org/).

4. Initialize the client-side app into the dynamic component
using the same data from the server-side.
This example passes the initial data to the client
as the argument of the main function.
Be mindful of potential XSS vulnerabilities.
43 changes: 43 additions & 0 deletions examples/dynamic/views/Content.js
@@ -0,0 +1,43 @@
var React = require('react');

var TodoList = React.createClass({
render: function() {
var i = 0;
var createItem = function(itemText) {
return <li key={i++}>{itemText}</li>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't matter at all but you could also take advantage of the fact that map will call this with 3 args, the 2nd being the index :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mind 💥

Sweet.

};
return <ul>{this.props.items.map(createItem)}</ul>;
}
});

var TodoApp = React.createClass({
getInitialState: function() {
return this.props;
},
onChange: function(e) {
this.setState({text: e.target.value});
},
handleSubmit: function(e) {
e.preventDefault();
var nextItems = this.state.items.concat([this.state.text]);
var nextText = '';
this.setState({items: nextItems, text: nextText});
},
render: function() {
return (
<div className="container">
<h3>TODO List</h3>
<TodoList items={this.state.items} />
<form className="form-inline" onSubmit={this.handleSubmit}>
<div className="form-group">
<input type="text" className="form-control" placeholder="Write task" onChange={this.onChange} value={this.state.text} />
&nbsp;
</div>
<button className="btn btn-primary">{'Add #' + (this.state.items.length + 1)}</button>
</form>
</div>
);
}
});

module.exports = TodoApp;
69 changes: 69 additions & 0 deletions examples/dynamic/views/Html.js
@@ -0,0 +1,69 @@
var React = require('react');
var Content = require('./Content');

module.exports = React.createClass({

render: function() {
var data = this.props.data;

// render the content as a dynamic react component
var contentHtml = React.renderToString(<Content {...data}/>);

/**
* re-render the content as json,
* for client-side app initialization
*
* NOTE on XSS prevention:
*
* This text will be placed into a script tag.
* It cannot be escaped,
* because it is intended to be raw javascript.
* Were the data object to contain the string, "</script>",
* the script tag would terminate prematurely.
* And two bad things would happen.
*
* 1. The client-side react application would not work.
* 2. A second script tag could then run arbitrary javascript.
*
* The former sucks a little but the latter sucks a lot.
* It would pwn you, game over, the site is no longer yours.
* There are three ways to thwart this scenario and you should do all of them:
*
* 1. Scrub input from users.
* Don't even let them enter data that is known to be potentially harmful.
* 2. Use a templating library that renders text by default.
* React does this, so YES!
* 3. Whenever you have to write raw user content into the document,
* block any content from breaking the current context.
*
* The third is what's going on with the `replace` function below.
* Because we're in a script tag context,
* we cannot allow the closing tag, "</script>", in our output.
* This is an old trick that breaks up the word "script" into a string contatenation.
* It works here because json always uses double quotes to escape strings.
*
* Properly escaping user data for raw output in html is tricky business.
* Whenever possible, avoid it.
* If avoidance is impossible,
* know what you are doing and good luck.
*/
var initScript = 'main(' + JSON.stringify(data).replace(/script/g, 'scr"+"ipt') + ')';

return (
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css"/>
</head>
<body>
<div id="content" dangerouslySetInnerHTML={{__html: contentHtml}}/>

<script src="/main.js"></script>
<script dangerouslySetInnerHTML={{__html: initScript}} />

</body>
</html>
);
}

});
10 changes: 10 additions & 0 deletions examples/dynamic/views/main.js
@@ -0,0 +1,10 @@
var React = require('react');
var Content = require('./Content');

module.exports = function (data, containerId) {
var container = document.getElementById(containerId || 'content');
React.render(
<Content {...data} />,
container
);
};