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
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
- Saga middleware
- 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.jsnpm 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
- core-js
- 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