Redux is a predictable state container for JavaScript apps. In this post we will try to learn about React-Redux. But, you may ask, is it valuable to learn redux 4.0 at July 2022. My answer is yes. Two days back, I got a client project which is based on redux 4.0 and I realised that new learners should also have knowledge of legacy redux.
What is Redux
Redux is an open-source javascript library to manage application state. It can be used in different Javascript frameworks but here we will discuss how to use it with React.
In react if we want to access state through out our application then we have to create a state container at our top level App component and pass as a prop to different child components. Passing props through child components is called prop drilling. In large size application we will face difficulty. So we are using Redux to store our state and manage it from any component.
Installation
npm install --save redux@4.0.5
The Gist
Store: The whole state of our app is stored in a single store. The shape of the state is upto us. It can be a primitive, an array, an object or even an immutable.js data structure. Remember, To change state we are not mutating the state object, but return a new object (later in this tutorial discussed in details).
// Create a redux store
import { createStore } from 'redux'
// Create Store
const store = createStore(mathReducer, {no:0})
// Subscribe to store
store.subscribe(() => {console.log("store updated...",store.getState())})
Remember, till now we have not created mathReducer. So it will through error. Don't worry for that we will create the reducer soon.
Action: The only way to change the state tree is to emit an action. An action is an object which contains two properties action type and optional payload property to pass value if required.
We can create a function for each action with will return object.
function add(number){
return{
type: "ADD",
payload: number // you can write any other property name in the place of payload
}
}
Reducer: These are pure functions which specify how the actions transform the state tree. This is a pure function which accepts (state, action) as parameter and returns state. We can say (state, action) => state signature.
// Create Reducers
function mathReducer(state, action){
switch(action.type){
case "ADD":
state = {...state, no : state.no + action.payload}
break;
}
return state;
}
Do you remember when updating an update we have to follow immutable approach. Thats why we are using spread operator ...
.
But wait, using spread operator will bring all the properties of previous state. So the no
property will repeat two times.
Yes. But the good thing is that the second no
property will overwrite the first one.
To change state we are not mutating the state object, but return a new object
Now it's time to combine all. Lets combine all in our App.js file.
- App.js
import { createStore } from 'redux'
console.log("Testing Redux");
// Create Reducers
function mathReducer(state, action){
switch(action.type){
case "ADD":
state = {...state, no : state.no + action.payload}
break;
case "SUB":
state = {...state, no : state.no - action.payload}
break;
}
return state;
}
// Create Store
const store = createStore(mathReducer, {no:0})
store.subscribe(() => {console.log("store updated...",store.getState())})
// Actions
function add(no){
return {type:"ADD", payload: no}
}
function sub(no){
return {type:"SUB", payload: no}
}
// Dispatch Actions
store.dispatch(add(1))
store.dispatch(add(3))
store.dispatch(sub(2))
function App() {
return (
<div className="App">
<h1>Redux 4.0</h1>
</div>
);
}
export default App;
- output on console
Multiple Reducers
What if we have multiple reducers ?
Don't worry redux has a facility for that. Because in real world applications there must be multiple reducers.
For this purpose we will use combineReducers
API from redux
.
import logo from './logo.svg';
import './App.css';
import { createStore, combineReducers } from 'redux'
console.log("Testing Redux");
// Initial State
const initialState = {no:0, personalDetails:{name:'', age:''}};
// Create Reducers
function mathReducer(state= initialState, action){
switch(action.type){
case "ADD":
state = {...state, no : state.no + action.payload}
break;
case "SUB":
state = {...state, no : state.no - action.payload}
break;
}
return state;
}
function objReducer(state= initialState, action){
switch(action.type){
case "SETNAME":
state = {...state, personalDetails:{...state.personalDetails, name : action.payload}}
break;
case "SETAGE":
state = {...state, personalDetails:{...state.personalDetails, age : action.payload}}
break;
}
return state;
}
// Create Store
const store = createStore(combineReducers({mathReducer, objReducer}))
store.subscribe(() => {console.log("store updated...",store.getState())})
// Actions
function add(no){
return {type:"ADD", payload: no}
}
function sub(no){
return {type:"SUB", payload: no}
}
function setName(name){
return {type:"SETNAME", payload: name}
}
function setAge(age){
return {type:"SETAGE", payload: age}
}
// Dispatch Actions
store.dispatch(add(1))
store.dispatch(add(3))
store.dispatch(sub(2))
store.dispatch(setName('Subrat'))
store.dispatch(setAge(45))
function App() {
return (
<div className="App">
<div>Redux 4.0</div>
</div>
);
}
export default App;
- output on console
Connect React with Redux
If you are using react version more then 17.0.2 then change react and react-dom version to 17.0.2.
"react": "^17.0.2",
"react-dom": "^17.0.2",
Also delete the testing packages or change to compatible versions.
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
Delete node_modules and run npm install
.
Change index.js
as below
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(
// <React.StrictMode>
// <App />
// </React.StrictMode>
// );
ReactDOM.render(<App />, window.document.getElementById('root'));
Till now we have not connected our Redux store to React. We need to install react-redux
. This package will create a bridge between React and Redux.
npm install --save react-redux@7.2.1
mapStateToProps
This is a custom user created function to map state of redux to prop of React. The name of function can be anything as per our choice but generally it is better to name it mapStateToProps to justify it's work.
function mapStateToProps(state){
return {
math : state.mathReducer,
obj: state.objReducer
}
}
now using connect
of redux we will connect it to React component.
let us create a class component in React.
- NumberComponent.js
import React, { Component } from 'react'
export default class NumberComponent extends Component {
render() {
return (
<div>
<h2>NumberComponent</h2>
<p>Number : {this.props.math.no}</p>
</div>
)
}
}
Here this.props.math.no
may through error because till now we have not connected. Let us create a container and connect to this component.
- NumberContainer.js
import { connect } from "react-redux"
import NumberComponent from "./NumberComponent"
function mapStateToProps(state){
return{
math: state.mathReducer
}
}
export default connect(mapStateToProps, null)(NumberComponent);
The null
in 2nd parameter of connect is for mapDispatchToProps
which we will discuss soon. Now let us create a function component and map our state to it. Because now a days function components are used more.
- PersonalDetailsComponent.js
import React from 'react'
export default function PersonalDetailsComponent(props) {
return (
<div>
<h2>Personal Details</h2>
<p>Name : {props.obj.personalDetails.name}</p>
<p>Age : {props.obj.personalDetails.age}</p>
</div>
)
}
- PersonalDetailsContainer.js
import { connect } from "react-redux"
import PersonalDetailsComponent from "./PersonalDetailsComponent"
function mapStateToProps(state){
return {
math : state.mathReducer,
obj: state.objReducer
}
}
export default connect(mapStateToProps,null)(PersonalDetailsComponent);
Now let us use both comonent in our app component and see the uses.
- We will import components from containers, because they are connected with our reducer.
- To access the store in all components we will wrap our App component with a PROVIDER.
- App.js
import logo from './logo.svg';
import './App.css';
import { createStore, combineReducers } from 'redux'
import {connect, Provider} from 'react-redux'
import PersonalDetails from './PersonalDetailsContainer'
import Number from './NumberContainer'
console.log("Testing Redux");
// Initial State
const initialState = {no:0, personalDetails:{name:'', age:''}};
// Create Reducers
function mathReducer(state= initialState, action){
switch(action.type){
case "ADD":
state = {...state, no : state.no + action.payload}
break;
case "SUB":
state = {...state, no : state.no - action.payload}
break;
}
return state;
}
function objReducer(state= initialState, action){
switch(action.type){
case "SETNAME":
state = {...state, personalDetails:{...state.personalDetails, name : action.payload}}
break;
case "SETAGE":
state = {...state, personalDetails:{...state.personalDetails, age : action.payload}}
break;
}
return state;
}
// Create Store
const store = createStore(combineReducers({mathReducer, objReducer}))
store.subscribe(() => {console.log("store updated...",store.getState())})
// Actions
function add(no){
return {type:"ADD", payload: no}
}
function sub(no){
return {type:"SUB", payload: no}
}
function setName(name){
return {type:"SETNAME", payload: name}
}
function setAge(age){
return {type:"SETAGE", payload: age}
}
// Dispatch Actions
store.dispatch(add(1))
store.dispatch(add(3))
store.dispatch(sub(2))
store.dispatch(setName('Subrat'))
store.dispatch(setAge(45))
function App() {
return (
<Provider store={store}>
<div className="App">
<h1>Redux 4.0</h1>
<PersonalDetails />
<Number />
</div>
</Provider>
);
}
export default App;
- output on browser
mapDispatchToProps
Now it's time to connect our dispatches as props
- PersonalDetailsContainer.js
import { connect } from "react-redux"
import PersonalDetailsComponent from "./PersonalDetailsComponent"
function mapStateToProps(state){
return {
math : state.mathReducer,
obj: state.objReducer
}
}
function mapDispatchToProps(dispatch){
return {
setName : (name) => dispatch({
type: "SETNAME",
payload: name
})
}
}
export default connect(mapStateToProps,mapDispatchToProps)(PersonalDetailsComponent);
- PersonalDetailsComponent.js
import React from 'react'
export default function HomeComponent(props) {
console.log("Home Component Props", props)
return (
<div>
<h2>HomeComponent</h2>
<p>Name : {props.obj.personalDetails.name}</p>
<p>Age : {props.obj.personalDetails.age}</p>
<button onClick={() => props.setName("Ramesh Jena")}>Set Name To Ramesh</button>
</div>
)
}
Similarly change NumberComponent.js and NumberContainer.js
- output on browser
Clicking on the Change Number by 5
button will add 5 each time to the number. Similaryly clicking on Set Name to Ramesh
will set the name to Ramesh Jena
Async Actions in Redux
Now let us change the setName function of App.js
function setName(name){
return dispatch => {
setTimeout(() =>{
dispatch({type:"SETNAME", payload: name})
}, 2000)
}
}
At beginning of program execution it should update the name 2 seconds late. But it will not because redux can't handle ASYNC operations. For this we have to use redux-thunk a middle ware.
npm install --save redux-thunk
in App.js import thunk and applyMiddleware.
import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
use it during create store.
const store = createStore(combineReducers({mathReducer, objReducer}),{}, applyMiddleware(thunk))
Promises in Redux
function setName(name){
return{
type: "SETNAME",
payload: new Promise((resolve, reject) => {
setTimeout(()=>{
resolve(name);
}, 2000)
})
}
}
npm install --save redux-promise-middleware
import { createStore, combineReducers, applyMiddleware } from 'redux'
import promise from 'redux-promise-middleware'
const store = createStore(combineReducers({mathReducer, objReducer}),{}, applyMiddleware(thunk, promise()))
Better project structure
Now have you noticed, we have written everything on App.js, which is not good for large scale applications. To make it scalable we can store our actions and reducers in separate folders.