How to Build a Single Page Application With React, Redux, Router
Join the DZone community and get the full member experience.
Join For FreeAs technology continues to evolve, single page applications (SPA), along with microservice-based backends become more and more popular. Single page application does not require page reloading during use once it has been loaded the first time, which means it's more user friendly 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 and well-organized. As illustrated 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 the logIn
function, we leverage redux-thunk middleware to perform asynchronous dispatches. This is really useful when you need to do conditional dispatches.
xxxxxxxxxx
import actionTypes from './Login.Constants';
// fake user data
const testUser = {
'name': 'juju',
'age': '24',
};
// login
export function logIn(opt, callBack) {
return (dispatch) => {
dispatch({
'type': actionTypes.LOGGED_DOING
});
setTimeout(function () {
fetch('https://github.com/', {
mode: 'no-cors'
})
.then((res) => {
dispatch({
'type': actionTypes.LOGGED_IN,
user: testUser
});
if (typeof callBack === 'function') {
callBack();
}
}).catch((err) => {
dispatch({
'type': actionTypes.LOGGED_ERROR,
error: err
});
});
}, 3000);
}
}
export function logOut() {
return {
type: actionTypes.LOGGED_OUT
}
}
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.
xxxxxxxxxx
import TYPES from './Login.Constants';
const initialState = {
isLoggedIn: false,
user: {},
status: null,
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case TYPES.LOGGED_DOING:
return {
state,
status: 'doing'
};
case TYPES.LOGGED_IN:
return {
state,
isLoggedIn: true,
user: action.user,
status: 'done'
};
case TYPES.LOGGED_OUT:
return {
state,
isLoggedIn: false,
user: {},
status: null
};
case TYPES.LOGGED_ERROR:
return {
state,
isLoggedIn: false,
user: {},
status: null
}
default:
return state;
}
}
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.
xxxxxxxxxx
import {
connect
} from "react-redux";
import {
bindActionCreators
} from "redux";
import * as rootActions from "../Root/Root.Actions";
import * as loginActions from "../Login/Login.Actions";
import Login from "./Login";
export default connect((state) => ({
isLoggedIn: state.userStore.isLoggedIn,
user: state.userStore.user,
status: state.userStore.status,
}), (dispatch) => ({
rootActions: bindActionCreators(rootActions, dispatch),
loginActions: bindActionCreators(loginActions, dispatch),
}))(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).
xxxxxxxxxx
handleLogin() {
if (!this.state.username || !this.state.password) {
return;
}
const opt = {
'name': this.state.username,
'password': this.state.password,
};
this.props.loginActions.logIn(opt, this.onSuccessLogin);
}
3.Redux Store
In the stores.js, we create a global store and register redux middleware, such as logger and thunk.
xxxxxxxxxx
//@flow
import thunk from 'redux-thunk';
import {
createStore,
applyMiddleware,
} from 'redux';
import {
persistStore,
persistReducer
} from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import reducers from './reducers';
const logger = store => next => action => {
if (typeof action === 'function') {console.log('dispatching a function');}
else {console.log('dispatching', action);}
const result = next(action);
console.log('next state', store.getState());
return result;
}
const middlewares = [
logger,
thunk
];
const createAppStore = applyMiddleware(middlewares)(createStore);
const persistConfig = {
key: 'root',
storage: storage,
transform: [],
};
const persistedReducer = persistReducer(persistConfig, reducers);
export default function configureStore(onComplete: () => void) {
const store = createAppStore(reducers);
persistStore(store, null, onComplete);
return store;
}
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.
xxxxxxxxxx
<Provider store={store}>
<Root/>
</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.
xxxxxxxxxx
<li><Link to={item.path}>{item.name}</Link></li>
xxxxxxxxxx
import React, {
Component
} from 'react';
import Login from './Login/Login.Container';
import Home from './Home/Home.Container';
import Root from './Root/Root.Container';
import {
IndexRoute,
Route,
Router,
} from 'react-router';
import {
browserHistory,
} from 'react-router';
export default function Routes() {
return (
<Router history={browserHistory}>
<Route path="/" component={Root}>
<IndexRoute component={Home} ></IndexRoute>
<Route path="home" component={Home}></Route>
<Route path="login" component={Login}></Route>
</Route>
</Router>
);
}
5. Service Worker
First of all, you may ask why we need a service worker. Suppose you have an e-commerce web site which is a single page application. 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.
xxxxxxxxxx
plugins: [
new SWPrecacheWebpackPlugin({
cacheId: "react-demo",
filename: "service-worker.js",
navigateFallback: "/index.html",
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
}),
]
Opinions expressed by DZone contributors are their own.
Comments