-
Notifications
You must be signed in to change notification settings - Fork 5
Tutorial 1 (Quick Start)
This tutorial runs through a starter setup with GroundControl.
Make sure you have Node installed. This project is built with version 5.0.0
.
Create a new directory and add a package.json
and webpack.config.js
:
mkdir ground-control-tutorial
cd ground-control-tutorial
touch package.json webpack.config.js
In your favorite text editor, open package.json
and add the following configuration:
{
"name": "ground-control-tutorial",
"version": "1.0.0",
"description": "Initial Ground Control Tutorial",
"scripts": {
"start": "nodemon --exec babel-node server.js"
},
"license": "MIT",
"dependencies": {
"ground-control": "^0.3.0",
"babel": "^5.8.34",
"babel-loader": "^5.4.0",
"express": "^4.13.3",
"nodemon": "^1.8.1",
"react": "^0.14.6",
"react-dom": "^0.14.6",
"react-router": "^2.0.0-rc5",
"redux": "^3.0.5",
"webpack": "^1.12.11",
"webpack-dev-middleware": "^1.4.0"
}
}
npm i
Open webpack.config.js
and add the following:
const path = require('path');
module.exports = {
devtool: 'source-map',
entry: path.join(__dirname, 'client.js'),
output: {
path: path.join(__dirname, '__build__'),
filename: 'bundle.js',
publicPath: '/__build__/'
},
module: {
loaders: [{ test: /\.js$/, exclude: /node_modules/, loader: 'babel' }]
}
};
We won't go into configuring Webpack as it's an in depth topic itself. Check out the Webpack documentation for more information. In summation, Webpack creates a reloadable development environment. It builds our application bundle every time we change one of its files.
We will note that we chose Babel to compile our JavaScript from ES6 to more browser-friendly ES5.
We'll setup a small server to host our application and its compiled JavaScript.
Create a server.js
file,
touch server.js
and add the following:
import express from 'express';
import webpack from 'webpack';
// We use our Webpack configuration to let our server know where to
// find our application build.
import webpackDevMiddleware from 'webpack-dev-middleware';
import WebpackConfig from './webpack.config';
function html() {
return (`
<!DOCTYPE html>
<html>
<head></head>
<body>
<h3>Hello!</h3>
<div id="app"></div>
<script src="/__build__/bundle.js" async></script>
</body>
</html>
`);
}
function render(req, res) {
res.send(html());
}
const app = express();
app.use(webpackDevMiddleware(webpack(WebpackConfig), {
publicPath: '/__build__/',
stats: { colors: true }
}));
// Render the same page on all routes.
app.get('*', render);
app.listen(8080, () => {
console.log('Server listening on http://localhost:8080.');
});
Next we'll make a placeholder client.js
file and run our server:
touch client.js
npm start
You should see your terminal output a few lines, the last one being: webpack: bundle is now VALID.
If you see that message, everything is setup correctly. Open your browser and go to localhost:8080
. You should see a simple Hello!
message appear on the screen. Let's begin to write our React-Router/Redux application.
Edit client.js:
import React from 'react';
import { render } from 'react-dom';
import { Router, Route, IndexRoute, IndexLink, Link, browserHistory } from 'react-router';
const AppComponent = React.createClass({
render() {
return (
<div style={{border:'1px solid green'}}>
<p>App</p>
<div style={{border:'1px solid purple'}}>{this.props.children}</div>
</div>
);
}
});
const IndexComponent = React.createClass({
render() {
return (
<p>Index component</p>
);
}
});
render((
<Router history={browserHistory}>
<Route path="/" component={AppComponent}>
<IndexRoute component={IndexComponent} />
</Route>
</Router>
), document.getElementById('app'));
With React-Router working, well setup a store that can react to our changes.
Edit client.js:
// ...
import { Router, Route, IndexRoute, IndexLink, Link, browserHistory } from 'react-router';
import { createStore } from 'redux';
const store = createStore((state = {}) => state);
// NOTE! you'll see this log a fair amount. On route change, we create a new reducer & replace the one the store uses.
// this triggers a few internal actions, like @@redux/INIT, @@anr/REHYDRATE_REDUCERS. it's expected.
const logCurrentState = () => console.log(JSON.stringify(store.getState()));
store.subscribe(logCurrentState);
logCurrentState();
// ...
GroundControl sits between React-Router and your application.
Edit client.js:
// ...
import { Router, Route, IndexRoute, IndexLink, Link, browserHistory } from 'react-router';
import GroundControl from 'ground-control';
// ...
render((
<Router history={browserHistory} render={(props) => (
<GroundControl {...props} store={store} />
)}>
<Route path="/" component={AppComponent}>
// ...
Let's create a reducer / action to interact with our application, and set it on the route.
Edit client.js,
// ...
const indexActions = {
increment: (count) => {
return { type: 'incr', payload: count };
}
};
const indexInitialState = { counter: 0 };
const indexReducer = (state = indexInitialState, action) => {
switch (action.type) {
case 'incr':
return { counter: state.counter + 1 };
default:
}
return state;
};
const IndexComponent = React.createClass({
render() {
return (
<p onClick={() => { this.props.dispatch(indexActions.increment(1)); }}>
Index component: {this.props.data.counter}
</p>
);
}
});
// ...
<Route path="/" component={AppComponent}>
<IndexRoute component={IndexComponent} reducer={indexReducer} />
</Route>
// ...
Let's expand upon our application, and add a 2nd route.
// ...
<p>App</p>
<IndexLink to="/">Home</IndexLink>
<Link to="/async-route">Async Route</Link>
<div style={{border:'1px solid purple'}}>{this.props.children}</div>
// ...
const AsyncComponent = React.createClass({
render() {
return (
<div>Async Component</div>
);
}
});
render((
// ...
<IndexRoute component={IndexComponent} reducer={indexReducer} />
<Route path="/async-route" component={AsyncComponent} />
// ...
Then, lets add a reducer:
// ...
const asyncActions = {
load: (data) => {
return { type: 'load', payload: data };
}
};
const asyncInitialState = { items: [] };
const asyncReducer = (state = asyncInitialState, action) => {
switch (action.type) {
case 'load':
state.items = action.payload;
break;
default:
}
return state;
};
const AsyncComponent = React.createClass({
render() {
return (
<div>
<p>Async Component</p>
<div>
{this.props.data.items.map((item, index) => (
<p key={index}>{item}</p>
))}
</div>
</div>
);
}
});
// ...
<Route path="/async-route" component={AsyncComponent} reducer={asyncReducer} />
// ...
For this route, we'd like to fetch data. Create a asyncEnter function and add it on the route.
// ...
const asyncFetchData = (done, { dispatch }) => {
setTimeout(() => {
dispatch(asyncActions.load(['a', 'b', 'c', 'd', 'e']));
done();
}, 1000);
};
const AsyncComponent = React.createClass({
// ...
<Route path="/async-route" component={AsyncComponent} reducer={asyncReducer} asyncEnter={asyncFetchData} loader={() => (<p>Loading...</p>)} />
// ...
We can improve our UI a bit, but using 'preview templates' instead of a blocking loader. Let's tell our client to render immediately.
// ...
const asyncFetchData = (done, { dispatch, clientRender }) => {
clientRender();
setTimeout(() => {
dispatch(asyncActions.load(['a', 'b', 'c', 'd', 'e']));
done();
}, 1000);
};
const AsyncComponent = React.createClass({
render() {
const items = this.props.loading ? ['x', 'y', 'z'] : this.props.data.items;
return (
<div>
<p>Async Component ({this.props.loading ? 'Loading...' : 'Loaded!'})</p>
<div>
{items.map((item, index) => (
<p key={index}>{item}</p>
))}
</div>
</div>
);
}
});
// ...
<Route path="/async-route" component={AsyncComponent} reducer={asyncReducer} asyncEnter={asyncFetchData} />
// ...
Let's update the last route we created to have its own nested routes. Let's add links and display loading status.
// ...
<div>
<p>Async Component ({this.props.loading ? 'Loading...' : 'Loaded!'})</p>
<Link to="/async-route">AsyncHome</Link>
<Link to="/async-route/async-nested">Async nested</Link>
<div>
{items.map((item, index) => (
<p key={index}>{item}</p>
))}
</div>
<div style={{border:'1px solid red'}}>
{this.props.children}
</div>
</div>
// ...
const asyncReducer2 = (state = 'nested-route-1') => state;
class AsyncComponent2 extends React.Component {
render() {
return (
<p>nested-route-1</p>
);
}
}
const asyncReducer3 = (state = 'nested-route-2') => state;
const AsyncComponent3 = ({ data }) => <p>{data}</p>;
render((
// ...
<Route path="/async-route" component={AsyncComponent} reducer={asyncReducer} asyncEnter={asyncFetchData}>
<IndexRoute component={AsyncComponent2} reducer={asyncReducer2}/>
<Route path="/async-route/async-nested" component={AsyncComponent3} reducer={asyncReducer3} />
</Route>
// ...
That was a good bit of work but thanks for bearing with it!
You...
- have one file (client.js) with React, Redux, React-Router, and now GroundControl.
- can organize your actual application code however you like (wouldn't recommend one file...).
- didn't have to think about initial reducer structure - you followed your routes.
- easily added new routes with minimal files - 1 file for actions, reducer, component.
- passed data into your components without worrying about actual nested structure.
- transitioned routes without having to reset state manually.
- controlled how data loads with sync/async route transitions.
Next, lets [break the app into multiple files](Tutorial 2 (Organize)), 1 file is getting out of hand.