Skip to content
This repository was archived by the owner on Nov 24, 2022. It is now read-only.

Tutorial 1 (Quick Start)

Nick Dreckshage edited this page Jan 23, 2016 · 5 revisions

This tutorial runs through a starter setup with GroundControl.

First Steps

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.

Setting Up the Server

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.

React-Router

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'));

Defining State

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();

// ...

Adding GroundControl

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}>

// ...

Creating an Action / Reducer

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>

// ...

Adding a 2nd Route

Let's expand upon our application, and add a 2nd route.

// ...

<p>App</p>
<IndexLink to="/">Home</IndexLink>&nbsp;
<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} />

// ...

Fetching Data (Blocking)

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>)} />

// ...

Fetching Data (Non-Blocking)

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} />

// ...

More Nested Routes

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>&nbsp;
  <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>

// ...

Done!

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.

Clone this wiki locally