React 16 with Webpack 4 and Babel 7 Part 2

Balkishan
9 min readOct 2, 2019

--

So now let us add react-router and the global state - Redux, on our react application. This is the continuation from my previous post.

As part of the tutorial, we will use the following frameworks/tools

  • Redux(4.0.4)
  • React-router-dom(5.0.1)

What are we going to discuss in this tutorial?

  • How to form routes to different components
  • How to manage the global state

The most important part of a website is that it has to be dynamic and navigation should be possible. We are going to use react-router to achieve the url navigation part.

So let us start with react router

npm i react-router-dom

Add BrowserRouter to the start of the project and the src/index should look like

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom'
import App from './App';ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('app')
);

Now add our routes to the App/index.js, It should look like

import React from 'react';
import { Route } from 'react-router-dom'
function App() {
return (
<div>
<h1>
This is the header
</h1>
<Route path="/user" render={() => <div>User</div>} />
<Route path="/home" render={() => <div>Home</div>} />
</div>
)
}
export default App;

Go ahead and run your code you will get the below error in the screenshot

Cannot GET error

We have a fix for this. Remember we added the html-webpack-plugin in first part. We should give a small configuration change. What happens is we are serving the index.html file from / So when there is a change in the url, it goes to some xyz.html but that is unavailable. So what happens is it throws a 404. But react-router-dom renders the new page in/ and will be served from / . So we enable historyApiFallback which will redirect us to the home page where actually the new page is rendered. Add the below code to webpack.config.js

devServer: {
historyApiFallback: true,
}

Now lets connect the app to our redux store. We have a lot of package like redux-thunk, redux-saga and many more. redux-saga is one good package available to handle all the sagas.

Oh wait, what is a saga? Lets go through the terms

Component — Responsible for displaying the data which is given to it (Dosent care about the store, It should be pluggable, Try not to have a local state in the cmponent)

Container — Its a higher order component will have local states and connected to the store.

Actions — It is basically a function which can be dispatched to start a particular action

Sagas — Responsible for making a particular call to fetch data from the backend servers (They can be triggered by firing an action)

Selectors — It derives data form the store and gives to the component

Reducers — Responsible to write data to the store (They can be triggered by firing an action)

Constants — Includes all the constants to be used in the containers

For now we will write some code in the containers and later we shall propogate it to the components

mkdir src/actions src/components src/constants src/containers src/reducers src/sagas src/selectors src/stores src/api

Let us move our app into the containers

mv src/App src/containerstouch src/actions/index.js src/constants/index.js src/reducers/index.js src/sagas/index.js src/selectors/index.js src/stores/index.js

So the folder structure should look like below

Now let us configure the store. Check out the official docs before we proceed to the store https://redux.js.org/recipes/configuring-your-store

To bring a single source of truth for our application and also to serve as a local store we use redux. For long, people have been using react-router-redux as we need to connect the redux store to the router and manage the router history. react-router-redux is now deprecated and we need to use react-router-dom in a different way as mentioned in the official docs https://reacttraining.com/react-router/web/guides/redux-integration

Let us have redux-logger middleware to see the changes in the redux states, else you can write your own middle-wares

touch src/stores/loggerMiddleware.js

src/stores/loggerMiddleware.js will look like

const loggerMiddleware = store => next => action => {
console.group(action.type)
console.info('Action', action)
let result = next(action)
console.log('Next State', store.getState())
console.groupEnd()
return result
}
export default loggerMiddleware;

Configuring the store is pretty easy. The final code snippet which exactly configures the store is given below

createStore(rootReducer, preloadedState, composedEnhancers);

All you need is

1. Your combined root reducers
2. The initial state that you will have to load
3. The enhances that you have added

In our case we have 2 middle-wares

  1. Saga middleware
  2. Redux logger
npm i redux-logger redux-saga
npm i -D redux-devtools-extension

stores/index.js will look like

import { applyMiddleware, createStore, compose } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import logger from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
// import loggerMiddleware from './loggerMiddleware';import rootReducer from '../reducers'
import * as sagas from '../sagas';
import { PROD_ENVS } from '../constants';
export default function configureStore(preloadedState) {
const middlewares = [];
// Push all the middlewares here
middlewares.push(createSagaMiddleware());
middlewares.push(logger);
// middlewares.push(loggerMiddleware);
const enhancers = [applyMiddleware(...middlewares)];// composeWithDevTools is only for the Development environment
let composedEnhancers = {};
if (PROD_ENVS.includes(process.env.NODE_ENV)) {
composedEnhancers = compose(...enhancers);
} else {
composedEnhancers = composeWithDevTools(...enhancers);
}
const store = createStore(rootReducer, preloadedState, composedEnhancers);
Object.values(sagas).forEach(sagaMiddleware.run.bind(sagaMiddleware));
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('../reducers', () => store.replaceReducer(rootReducer))
}
return store;
}

No we have to change our src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import App from './containers/App';
import configureStore from './stores';
ReactDOM.render(
<Provider store={configureStore({})}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('app')
);

Now lets write the container and sagas

touch src/containers/App/actions.js
touch src/containers/App/reducers.js
touch src/containers/App/sagas.js

Now our component needs to be a class component, It can’t be functional, Because it will have to interact with the redux store.

import React from 'react';
import { withRouter, Route } from 'react-router-dom';
import { connect } from 'react-redux';
import * as _ from 'lodash';
import { getUserList } from './actions';
class App extends React.Component {
constructor(props) {
super(props);
this.props.getUserList();
}
render() {
const { userList } = this.props;
return (
<div>
<h1>
This is the header
</h1>
<div>
Fetched {userList.length} users
</div>
<Route path="/user" render={() => <div>User</div>} />
<Route path="/home" render={() => <div>Home</div>} />
</div>
);
}
}
const mapStateToProps = (state) => {
return {
userList: _.get(state, 'app.userList', []),
};
};
const mapDispatchToProps = (dispatch) => {
return {
getUserList: () => dispatch(getUserList()),
};
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App));

Let us display the list of users when the page loads. I have an api from jsnoplaceholder.com, which provides us with a sample GET request.

https://jsonplaceholder.typicode.com/users

App/actions.js

export function getUserList() {
return {
type: 'GET_USERS'
}
}
export function getUserListSuccess(userList) {
return {
type: 'GET_USERS_SUCCESS',
userList
}
}
export function getUserListFail() {
return {
type: 'GET_USERS_FAIL'
}
}

App/reducers.js

export function AppReducer(state = {}, action) {
switch (action.type) {
case 'GET_USERS_SUCCESS':
return {
...state,
userList: action.userList,
}
case 'GET_USERS_FAIL':
return {
...state,
userList: [],
}
default:
return state;
}
}

App/sagas.js

import {
put,
call,
takeLatest,
} from 'redux-saga/effects';
import { getUserListSuccess, getUserListFail } from './actions';
import { get } from '../../api';
function* getUserList() {
try {
const userList = yield call(get, 'https://jsonplaceholder.typicode.com/users');
yield put(getUserListSuccess(userList))
} catch (e) {
yield put(getUserListFail());
}
}
export function* getUserListSaga() {
yield takeLatest('GET_USERS', getUserList);
}
export default [
getUserListSaga,
];

If we see here we have used the common api for all the fetch operations.

import { get } from '../../api';

So we should configure the common CRUD api’s

mkdir src/api
touch src/api/index.js
npm i lodash ramda

api/index.js

import * as R from 'ramda';const get = function get(endpoint, headers, options = {}) {
const commonHeaders = {
'Content-Type': 'application/json',
};
const commonOptions = {
method: 'GET',
mode: 'cors',
credentials: 'include',
...options,
};
return fetch(endpoint,
{
...commonOptions,
headers: Object.assign({}, commonHeaders, headers),
})
.then((response) => {
if (R.prop('ok', response)) {
return response.json();
}
return null;
});
};
export {
get
};

Now it’s time to combine all the reducers and sagas.

sagas/index.js

export { getUserListSaga } from "../containers/App/sagas";

reducers/index.js

import { combineReducers } from 'redux';
import { AppReducer } from '../containers/App/reducers';
const rootReducer = combineReducers({
app: AppReducer,
});
export default rootReducer;

Now we are ready to launch the app. Go ahead and give npm start and the build should be successful.

But we get an error regeneratorRuntime is not defined

What is this? See we have use async/await calls in our sagas. Any was babel needs to understand that. This is not understandable by the browser even.
So we need some tool to tell babel to convert this to browser understanding format. So we use @babel/polyfill.

This is deprecated code, skip to For babel > 7.4.0 to continue the flow

For babel < 7.4.0

npm i --save @babel/polyfill

And we need to add the below code to our main js file, to enable the runtime generators.

import "core-js/stable";
import "regenerator-runtime/runtime";

For babel > 7.4.0

@babel/polyfill is deprecated, We need to address 2 issues

  1. core-js
  2. regenerator-runtime

Here is the solution https://babeljs.io/docs/en/babel-plugin-transform-runtime All we need to to use a different package — @babel/plugin-transform-runtime

npm i -D @babel/plugin-transform-runtime
npm i @babel/runtime

After adding the configuration, .babelrc will look like

{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
// "absoluteRuntime": false,
"corejs": false,
// "helpers": true,
"regenerator": true,
// "useESModules": false
}
]
]
}

Fine we are good to go now. Start the app again and go the browser for the updates. But still we have some issue.. Guess what? Lets open the dev tools in chrome, check the screenshot.

1. You can see multiple network calls, Everything goes more than one time
2. Favicon is missing

Anyway this is not going to affect the performance, but this should be solved.
Well if you remember we added the below line in index.html file in Part 1

<script type="text/javascript" src="bundle.js"></script>

You can see this gets added to the index.html if you run npx webpack to check if the build succeeds
This is because of the configuration in the webpack.config.js. Entry and output will take care of including the bundle.js file in the final code.
So we need not add this explicitly. Let us remove this. This line will be injected into the code when web-pack builds.

Add a favicon.ico to the dist folder.

Now we are clear. Let’s roll ! Check out the screenshot below.

Follow the commits in the git repo to get step by step code updates.

To begin with the redux-saga https://redux-saga.js.org/docs/introduction/BeginnerTutorial.html

Feel free to verify the official docs from https://reacttraining.com/react-router/web/guides/quick-start

If you think you have missed the part 1 of the initial setup. No worries we have it here https://medium.com/@kishan020696/react-16-with-webpack-4-and-babel-7-7befed102817

--

--