How to build a single page application(UI) by using React, Redux, Router

As technology continues to evolve, single page UI application(SPA) along with micro services backend becomes more popular.  Single page application does not require page reloading during use once it was loaded in the first time, which means it's more user friendly and it's faster than legacy UI made by something like JSP, PHP, ASP etc.

There are various techniques available that enable the browser to retain a single page even when the application requires server communication. In this article, I'm going to introduce how to make that with React. You need to have a quick look at basic tutorials in regards to npm, react components, babel, webpack. The code is available in Github:

https://github.com/liqili/react-redux-router-webpack-demo

1.Architecture chart

This is an UI MVC architecture chart. The view is React class component with its own states, constants, actions(events), reducers(event handlers), containers(connect to Redux global store). The model and controller are Redux store, which acts as a global centralized manager, dispatching actions and executing reducers. The state change will in turn result in React components updating.


2.React components

By leveraging Redux concepts, we can make React components clean, well-organized. As illustrated as below, we can see there are actions, constants, containers, reducers, and main class in each component folder.

Let's take login page as an example.

2.1 Actions

Actions are plain JavaScript objects. Actions must have a type property that indicates the type of action being performed. You send them to the store using store.dispatch(). The functions that dispatch actions are called action creators, such as logOut and logIn. For logIn function,  we leverage redux-thunk middleware to perform asynchronous dispatches. This is really useful when you need to do conditional dispatches.

JavaScript


1
import actionTypes from './Login.Constants';
2
3
// fake user data
4
const testUser = {
5
    'name': 'juju',
6
    'age': '24',
7
};
8
9
// login
10
export function logIn(opt, callBack) {
11
    return (dispatch) => {
12
        dispatch({
13
            'type': actionTypes.LOGGED_DOING
14
        });
15
        setTimeout(function () {
16
            fetch('https://github.com/', {
17
                mode: 'no-cors'
18
            })
19
                .then((res) => {
20
                    dispatch({
21
                        'type': actionTypes.LOGGED_IN,
22
                        user: testUser
23
                    });
24
                    if (typeof callBack === 'function') {
25
                        callBack();
26
                    }
27
                }).catch((err) => {
28
                    dispatch({
29
                        'type': actionTypes.LOGGED_ERROR,
30
                        error: err
31
                    });
32
                });
33
        }, 3000);
34
35
    }
36
}
37
38
export function logOut() {
39
    return {
40
        type: actionTypes.LOGGED_OUT
41
    }
42
}
43



2.2 Reducers

The reducer is a pure function that takes the previous state and an action, and returns the next state. The concept comes from map-reduce. Redux also provides a combineReducers helper function to merge separate reducing functions from different components into a single reducing function so that we can pass to createStore.

JavaScript


1
import TYPES from './Login.Constants';
2
3
const initialState = {
4
    isLoggedIn: false,
5
    user: {},
6
    status: null,
7
};
8
9
export default function reducer(state = initialState, action) {
10
11
    switch (action.type) {
12
        case TYPES.LOGGED_DOING:
13
            return {
14
                ...state,
15
                status: 'doing'
16
            };
17
18
        case TYPES.LOGGED_IN:
19
            return {
20
                ...state,
21
                isLoggedIn: true,
22
                user: action.user,
23
                status: 'done'
24
            };
25
26
        case TYPES.LOGGED_OUT:
27
            return {
28
                ...state,
29
                isLoggedIn: false,
30
                user: {},
31
                status: null
32
            };
33
        case TYPES.LOGGED_ERROR:
34
            return {
35
                ...state,
36
                isLoggedIn: false,
37
                user: {},
38
                status: null
39
            }
40
41
        default:
42
            return state;
43
    }
44
}



2.3 Containers

Container components are used to connect/subscribe to Redux store, which means it can bind to redux store state change and actions.

JavaScript


1
import {
2
    connect
3
} from "react-redux";
4
import {
5
    bindActionCreators
6
} from "redux";
7
import * as rootActions from "../Root/Root.Actions";
8
import * as loginActions from "../Login/Login.Actions";
9
import Login from "./Login";
10
11
export default connect((state) => ({
12
    isLoggedIn: state.userStore.isLoggedIn,
13
    user: state.userStore.user,
14
    status: state.userStore.status,
15
}), (dispatch) => ({
16
    rootActions: bindActionCreators(rootActions, dispatch),
17
    loginActions: bindActionCreators(loginActions, dispatch),
18
}))(Login);



2.4 React components

In this example, we still use React class components, however, since the latest React version has introduced hooks to functional components, it's no longer recommended to use class components. We can dispatch redux actions just like calling a methods thanks to react-redux connect component(illustrated in 2.3).

JavaScript

1
handleLogin() {
2
        if (!this.state.username || !this.state.password) {
3
            return;
4
        }
5
        const opt = {
6
            'name': this.state.username,
7
            'password': this.state.password,
8
        };
9
        this.props.loginActions.logIn(opt, this.onSuccessLogin);
10
    }
11



3.Redux Store

In the stores.js, we create a global store and register redux middlewares such as logger and thunk.

JavaScript


1
//@flow
2
import thunk from 'redux-thunk';
3
import {
4
    createStore,
5
    applyMiddleware,
6
} from 'redux';
7
import {
8
    persistStore,
9
    persistReducer
10
} from 'redux-persist';
11
import storage from 'redux-persist/lib/storage';
12
import reducers from './reducers';
13
14
const logger = store => next => action => {
15
    if (typeof action === 'function') {console.log('dispatching a function');}
16
    else {console.log('dispatching', action);}
17
    const result = next(action);
18
    console.log('next state', store.getState());
19
    return result;
20
}
21
22
const middlewares = [
23
    logger,
24
    thunk
25
];
26
27
const createAppStore = applyMiddleware(...middlewares)(createStore);
28
const persistConfig = {
29
    key: 'root',
30
    storage: storage,
31
    transform: [],
32
};
33
const persistedReducer = persistReducer(persistConfig, reducers);
34
35
export default function configureStore(onComplete: () => void) {
36
    const store = createAppStore(reducers);
37
    persistStore(store, null, onComplete);
38
    return store;
39
}
40


All React container components need access to the Redux store so they can subscribe to it. It's recommended to use <Provider> to make the store available to all container components in the application without passing it explicitly. You only need to use it once when you render the root component. Please check index.js to see how to pass the Redux store.

JavaScript

1
<Provider store={store}>
2
  <Root/>
3
</Provider>



4.Routers

Routers play a key role in single page application. Its most basic responsibility is to dynamically render some UI(partly) when its path matches the current URL. It needs Link tag to work together with html navigation tags.

<li><Link to={item.path}>{item.name}</Link></li>
JavaScript




5. Service Worker

First of all, you may ask why we need service worker. Suppose you have an e-commerce web site which is a single page application(UI). You navigate to shopping cart from index page, and you won't have any issues thanks to UI router mechanism. But if you open a new window and paste  URL like "localhost/cart/" to the browser to visit the shopping cart, then it will send the request - "localhost/cart/" to backend to fetch the page. Obviously you will get 404 error. Service worker comes out for this scenario.

In my example, I use webpack sw-precache-webpack-plugin to handle that.

JavaScript

1
plugins: [
2
        new SWPrecacheWebpackPlugin({
3
            cacheId: "react-demo",
4
            filename: "service-worker.js",
5
            navigateFallback: "/index.html",
6
            staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
7
        }),
8
    ]



Comments

Popular posts from this blog

Build J2EE micro services architecture by using Spring Boot, Spring Cloud, Spring Security OAuth2, KeyCloak

NGINX and HTTPs with Let’s Encrypt, Certbot, and Cron dockerization in production

Vault Cubbyhole authentication and its integration with Spring framework