[go: up one dir, main page]

DEV Community

Sebastian G. Vinci
Sebastian G. Vinci

Posted on

React JS Web Site Example (Almost like real life).

I've been trying to use react on my personal projects for a couple weeks now, but I found out that there isn't one example on the internet (that I could find) that ressembles what I want in a real life scenario.

Asynchronous HTTP requests, loading animations, error pages, etc. None of those things are covered by one concise example that can be found on the first two pages of google.

Having said that, I took one example that took me far enough, and started researching and building on top of it.

What are we going to do?

We are going to build a simple To Do List web application.

To do this, we are going to build a very simple REST API in Node.js using rest-api-starter, and a web site based on React.JS, Redux and Bootstrap.

What am I going to need to follow this tutorial?

First, a Node.js 6 installation, an IDE and a browser (which you probably have already, as you are reading this). Instructions on how to install Node.js can be found here.

Second, a Python 2.7 installation. If you are on a Mac OS or an Ubuntu based system, you already have it. Instructions on how to install Python can be found here.

All the commands I'll provide to install, run and do stuff were tested on Linux Mint 18. They'll probably work on Mac OS without any issue. If you're working on windows I'm really sorry.

Can we start coding already?

Allright, first of all, let's make our directories.

$ mkdir todo-api
$ mkdir todo-site
Enter fullscreen mode Exit fullscreen mode

API project

Now, let's start with the API. We are going to cd to the API directory, and run npm init.

$ cd todo-api
$ npm init
Enter fullscreen mode Exit fullscreen mode

You can leave all the defaults.

Now we have a node project there, we are going to install rest-api-starter and uuid (for id generation and stuff).

$ npm install --save rest-api-starter uuid
Enter fullscreen mode Exit fullscreen mode

Now, rest-api-starter requires a tiny configuration file on a subdirectory called config.

$ mkdir config
$ cd config && touch default.json
Enter fullscreen mode Exit fullscreen mode

The config/default.json file should look exactly like the one below:

{
  "app": {
    "http": {
      "port": 8100,
      "host": "0.0.0.0",
      "queue": 10,
      "secret": "",
      "transactionHeader": "X-REST-TRANSACTION"
    },
    "log": {
      "level": "info",
      "transports": [
        {
          "type": "console"
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's code our rest API. We need CORS support to be able to easily develop on our local environment and three handlers:

  • POST /todos: Create an item.
  • GET /todos: Retrieve all items.
  • PATCH /todos/:id: Mark an item as done or undone.

Also, an OPTIONS handler for each path should be implemented for CORS support. So, our index.js file will look like this:

const uuid = require('uuid');
const serveBuilder = require('rest-api-starter').server;
const todos = [];

const router = (app) => {

    app.use(function(req, res, next) {
        res.header("Access-Control-Allow-Origin", "*");
        res.header("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
        res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        next();
    });

    app.options('/todos', (request, response) => response.status(200).send());

    app.post('/todos', (request, response) => {
        const todo = {
            'id': uuid.v4(),
            'isDone': false,
            'text': request.body.text
        };
        todos.push(todo);
        response.send(todo);
    });

    app.get('/todos', (request, response) => {
        response.send(todos);
    });

    app.options('/todos/:id', (request, response) => response.status(200).send());

    app.patch('/todos/:id', (request, response) => {
        let result = null;
        todos.forEach((todo) => {
            if (todo.id === request.params.id) {
                todo.isDone = !todo.isDone;
                result = todo;
            }
        });

        if (!result) {
            response.status(404).send({'msg': 'todo not found'});
        } else {
            response.send(result);
        }
    });

};

serveBuilder(router);
Enter fullscreen mode Exit fullscreen mode

Now, add "start": "node index.js" to the scripts section of your package.json file to start the server. By running npm run start on the root of the API project, you'll have your server listening on http://localhost:8100.

Site project

Now we're going to cd to the site project and run an npm init there. Defaults are fine here too.

$ cd todo-site
$ npm init
Enter fullscreen mode Exit fullscreen mode

And now, we install the dependencies we need:

$ npm install --save babel-core babel-loader babel-preset-es2015 babel-preset-react bootstrap jquery superagent webpack react react-dom react-redux redux redux-thunk style-loader css-loader
Enter fullscreen mode Exit fullscreen mode

Webpack

We'll be using webpack to transpile and unify all the code into una file called bundle.js, so it will be convenient to add "build": "webpack --debug" and "serve": "npm run build && python -m SimpleHTTPServer 8080" to the scripts section in our package.json.

Now we'll need a webpack.config.js.

const webpack = require('webpack');

module.exports = {
    entry: {
        main: './src/app.js'
    },
    output: {
        path: __dirname,
        filename: 'bundle.js'
    },
    module: {
        loaders: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
                query: { presets: [ 'es2015', 'react' ] }
            },
            {
                test: /\.css$/,
                loader: "style-loader!css-loader"
            },
            {
                test: /\.(png|jpg|gif|ttf|svg|woff|woff2|eot)$/,
                loader: "url-loader"
            }
        ]
    },
    plugins: [
        new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery",
            bootstrap: "bootstrap"
        })
    ]
};
Enter fullscreen mode Exit fullscreen mode

This webpack configuration transpiles all the javascript files that are using ES6 and JSX, and then puts them together, with all their dependencies, in one big file called bundle.js.

If any stylesheet is required from src/app.js, it will import it and add it to the bundle (following any imports made from the stylesheets) and the generated bundle script will add a <style> tag to the HTML.

It also uses the ProvidePlugin to expose JQuery and bootstrap, so we can forget about importing them.

Stylesheets

Now, let's start with some structure. Let's create a directory called css in the root of the project and add the following app.css.

@import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
Enter fullscreen mode Exit fullscreen mode

That stylesheet just imports bootstrap, but you can add custom style and import any stylesheet you want there. That should be the entrypoint for all the stylesheets in the project.

HTML. Site entrypoint.

Then, we create our index.html in the project.

<!DOCTYPE html>
<html>
    <head>
        <title>Todo List</title>

        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    </head>
    <body>
        <div id="app"></div>

        <script src="bundle.js"></script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

This is a pretty simple HTML file. It has a title, the viewport recommended by bootstrap, a div with the id app and the import of our bundle.

That div called app will be our application container. We'll tell react to render its components there.

React Components

Let's write our React.js components. A React component is an independent piece of code that receives some props and renders HTML from that props. It should JUST be React, a component's code should know nothing about Redux. Just presentation. (I can't stress this enough).

Create a directory called src on the root of the project, and write the code below to a file named components.js.

import React from 'react';

function Todo(props) {
    const { todo } = props;
    if (todo.isDone) {
        return <del>{todo.text}</del>
    } else {
        return <span>{todo.text}</span>
    }
}

function TodoList(props) {

    const { todos, toggleTodo, addTodo } = props;

    const  => {
        event.preventDefault();

        const textInput = document.getElementById('todo-input');

        const text = textInput.value;

        if (text && text.length > 0) {
            addTodo(text);
        }

        textInput.value = '';
    };

    const toggleClick = id => event => toggleTodo(id);

    return (
        <div className='todo-list-container'>
            <div className="panel panel-default">
                <div className="panel-body">
                    <form 
                        <div className="form-group">
                            <label>To Do Text: </label>
                            <input id="todo-input" type='text'
                                   className='todo-input form-control'
                                   placeholder='Add todo' />
                        </div>
                        <button type="submit" className="btn btn-default">Submit</button>
                    </form>
                </div>
            </div>
            {
                todos.length > 0 ?
                    <div className='todo-list list-group'>
                        {todos.map(t => (
                            <a key={t.id}
                                className='todo-list-item list-group-item'
                                
                                <Todo todo={t} />
                            </a>
                        ))}
                    </div> :
                    <div className="alert alert-info" role="alert">ToDo list is empty.</div>
            }
        </div>
    );
}

function Layout(props) {
    return (
        <div className='container'>
            <div className='row'>
                <div className='col-lg-6 col-lg-offset-3'>
                    <div className='page-header'>
                        <h1>To Do List <small>Keep it organized.</small></h1>
                    </div>
                    {props.children}
                </div>
            </div>
        </div>
    )
}

function ProgressBar(props) {
    const { completed } = props;

    const style = { 'width': completed + '%'};

    return (
        <div className="progress">
            <div className="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow={completed} aria-valuemin='0' aria-valuemax='100' style={style}>
                <span className="sr-only">{completed}% Complete</span>
            </div>
        </div>
    )
}

export function TodoPage(props) {

    const {state, toggleTodo, addTodo, retrieveTodos } = props;

    if (state.error) {
        return (
            <Layout>
                <div className="alert alert-danger" role="alert">{state.error.toString()}</div>
                <input className='retry-button btn btn-default' type='button' value='Retry' 
            </Layout>
        );
    } else if (state.initialized) {
        return (
            <Layout>
                <TodoList todos={state.todos} toggleTodo={toggleTodo} addTodo={addTodo} />
            </Layout>
        )
    } else {
        retrieveTodos();
        return (
            <Layout>
                <ProgressBar completed="45"/>
            </Layout>
        );
    }

}

Enter fullscreen mode Exit fullscreen mode

That's our presentation layer. We export one function, called TodoPage, which uses some components only available inside the module.

These components receive the application's state, and three actions: toggleTodo, addTodo, retrieveTodos. The components don't know what they do, they just know how to invoke them, and they don't even care about a return value.

Notice that the components receive the state and the actions, and just cares about how the state is displayed, and how are those actions mapped to HTML events.

API Client

Now, let's write our API client using superagent and ES6 promises. under a directory called src created on the root of our project write the following code on a file called client.js.

import * as superagent from "superagent";

export function get() {

    return new Promise((resolve, reject) => {
        superagent.get("http://localhost:8100/todos")
            .end((error, result) => {
                error ? reject(error) : resolve(result.body);
            });
    });

}

export function add(text) {

    return new Promise((resolve, reject) => {
        superagent.post("http://localhost:8100/todos")
            .send({'text': text})
            .end((error, result) => {
                error ? reject(error) : resolve(result.body);
            });
    });

}

export function toggle(id) {

    return new Promise((resolve, reject) => {
        superagent.patch("http://localhost:8100/todos/" + id)
            .end((error, result) => {
                error ? reject(error) : resolve(result.body);
            });
    });

}
Enter fullscreen mode Exit fullscreen mode

That module exports three functions:

  • get: Executes a GET request to /todos in our API to retrieve all To Do items.
  • add: Executes a POST request to /todos in our API to add a To Do item.
  • toggle: Executes a PATCH request to /todos/:id to change the isDone flag of that item.

Redux Actions

Let's talk about actions...

Actions, in Redux, are pieces of information that get sent to the store. These payloads trigger modifications on the state of the application.

Actions are basically Redux's way of saying "Hey! This happened!".

WARNING: Not actual modifications, the state of the application shoud be treated as an immutable object. You should never modify the state, but copy it, change the copy and keep going. More on it further down.

Actions are generated via action builders. These builders are functions that get invoked with some information and return the action, which is sent to the store via a dispatch function provided by Redux.

An interesting concept, necessary for real world applications, are asynchronous actions. These aren't actually just a piece of information, but another function that receives the dispatch function as parameters and, after some asynchronous operations, dispatches another action. Let's explain it with some code.

Write the following code on a file called actions.js under the src directory.

import { get, add, toggle } from './client';

export function addTodo(text) {
    return (dispatch) => {
        add(text)
            .then(get)
            .then((todos) => dispatch(receiveTodos(todos)))
            .catch((err) => dispatch(error(err)));
    };
}

export function toggleTodo(id) {
    return (dispatch) => {
        toggle(id)
            .then(get)
            .then((todos) => dispatch(receiveTodos(todos)))
            .catch((err) => dispatch(error(err)));
    };
}

export function retrieveTodos() {
    return (dispatch) => get()
        .then((todos) => dispatch(receiveTodos(todos)))
        .catch((err) => dispatch(error(err)))
}

function receiveTodos(todos) {
    return {
        type: 'RECEIVE_TODOS',
        payload: todos
    }
}

function error(err) {
    return {
        type: 'ERROR',
        payload: err
    };
}
Enter fullscreen mode Exit fullscreen mode

We here are defining all the behavior of our application.

Our application has to retrieve To Do items from the API, toggle them and create them. These actions are asynchronows.

  • The addTodo action builder returns an asynchronous action that, after posting a new To Do item to the API, and retrieving all the To Do items again, dispatches the receiveTodos action. On error, it dispatches the error action.

  • The toggleTodo action builder returns an asynchronous action that, after toggling the To Do item on the API, and retrieving all the items again, dispatches the receiveTodos action. On error, it dispatches the error action.

  • The retrieveTodos action builder returns an asynchronous action that, after retrieving all the To Do items from the API, dispatches the receiveTodos action. On error, it dispatches the error action.

Notice that these (not as they are defined here, we'll see how) are the actions that are used by our components to handle HTML events.

The other two actions are ordinary actions, that receive some data and returns a payload.

  • The receiveTodos action builder returns an action of type RECEIVE_TODOS with the retrieved todos as payload.

  • The error action builder returns an action of type ERROR with the received error as payload.

This might sound confusing. I think Redux is not an easy to understand state manager, its concepts are pretty hard to understand, but if you put this in practice and read the code you'll end up liking it a lot.

Redux Reducer

This takes us to the reducers. A reducer is a function that receives the current state of the application and an action. As stated before, an action is a way of saying that something happened, and a reducer grabs that event/information and does what it need to do to the state to impact that event on it.

Basically, they receive the current state of the application and an action that was performed (an event or something, like a user click for example) and return the new state of the application.

Let's see more code. Write the following code on a file called reducer.js under the src directory.


const init = {'todos': [], 'error': false};

export default function(state=init, action) {
    switch(action.type) {
        case 'RECEIVE_TODOS':
            return {'todos': action.payload, 'error': false, 'initialized': true};
        case 'ERROR':
            return {'todos': [], 'error': action.payload, 'initialized': true};
        default:
            return state;
    }
}
Enter fullscreen mode Exit fullscreen mode

This reducer is defining the initial state of the application and taking care of handling the actions it receives.

If the action it received is of type RECEIVE_TODOS, it returns the new state, ensuring that error is false, initialized is true and todos contains the received todos.

If the action it received is of type ERROR, it returns the new state, ensuring that error contains the ocurred error, initialized is true and todos is an empty array.

If the action it received has no handler, it just passes through the current state of the application as no changes are to be applied.

Sorry I repeat myself so much, but this concept took me a while: React components receive Redux's action builders, and ivoke them on HTML events. These events get dispatched to Redux's reducers to do what they have to do to the state based on the information provided by the action.

Container Components

Another new concept: containers. Containers are a type of component, they are called Container Components. They do the connection between React components (which are just presentational components and know nothing about redux), and redux's actions and state.

They basically wrap the react component, and grabs the state and the actions and maps them to props.

Let's see the code. Write the following code in a file called containers.js under the src directory.

import { connect } from 'react-redux';
import * as components from './components';
import { addTodo, toggleTodo, retrieveTodos } from './actions';

export const TodoPage = connect(
    function mapStateToProps(state) {
        return { state: state };
    },
    function mapDispatchToProps(dispatch) {
        return {
            addTodo: text => dispatch(addTodo(text)),
            toggleTodo: id => dispatch(toggleTodo(id)),
            retrieveTodos: () => dispatch(retrieveTodos())
        };
    }
)(components.TodoPage);
Enter fullscreen mode Exit fullscreen mode

It grabs our TodoPage, our actions and the state, and puts them into props, for our component to see. This is where everything is glued together.

Web Application Start Up

Let's go to our application entry point now. Write the following code in a file called app.js under src.

import '../css/app.css';

import React from 'react';
import { render } from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import reducer from './reducer';
import { TodoPage } from './containers';

const store = createStore(reducer, applyMiddleware(thunk));

document.addEventListener("DOMContentLoaded", function() {

    render(
        <Provider store={store}>
            <TodoPage />
        </Provider>,
        document.getElementById('app')
    );

});
Enter fullscreen mode Exit fullscreen mode

This file is importing our css entry point file, our reducer and the TodoPage container (not the component, the container).

Then, it creates the Redux store (basically, where te state lives). You might have noticed that our reducer is not handling any of our asynchronous actions, thats why we are passing that applyMiddleware(thunk) to createStore. redux-thunk takes care of handling asynchronous actions just like that.

We now wait for the DOM to be loaded, and then calling React's render function. This function receives a component and the container HTML element (that's our div#app from index.html).

The component we are passing to the render function is a Provider tag, with only one child (this is important, it can not have more than one child), which is our TodoPage container component. We are passing our store to the Provider tag by the way.

You're ready to go

We now can run npm run serve in the root of the site project, and npm run start in the root of the API project. Now we can visit http://localhost:8080/ and use our To Do list.

Conclusion

I find this pair (React, Redux) to have a pretty complex ramp up, but once you get the hang of it, applications are written quickly and the code looks great too. Yeah, it's a lot of boiler plate sometimes, but it looks nice and it actually performs pretty well too.

I come from the JQuery world, then moved on to Angular.JS, and now I moved to React.JS and Redux and I actually like it.

You can find the code to this example here.

See you in the comments!

Top comments (2)

Collapse
 
caseyreeddev profile image
Casey Reed

I'm fairly new to React, but is there a reason you use document.getElementById() in your component.js rather than using the ref attribute? For example, adding something like ref={(input) => { this.textInput = input; }} onto the input in your JSX.

Fantastic article, by the way! Will certainly be reading and re-reading down the line.

Collapse
 
svinci profile image
Sebastian G. Vinci

The recommended way is to use event handlers.

Having a function defined as function onChange(event), and passing it to the element as >.

The thing here is that I didn't want to make it more complex, because it's just one input and one button, so that did the trick. If there were more elements involved, with a more complex form, I would've handled it using event handlers.

You gave me an idea, I'll try to write a post on how to handle complex forms during this week!

Thanks for pointing that out!
Regards.