- A Predictable State Container for JS Apps.
- Redux is not limited to React and can be used in Vanilla JS projects.
-
A reducer is a function which takes two arguments — the current state and an action — and returns a new state.
-
A Reducer Function is a perfect example of a Pure Function.
-
We can express the idea in a single line, as an almost valid function:
const reducer = (state, action) => newState;
-
Building a Counter using Reducer:
// state : An object to store the states // E.g. state = { count : 23 }, state = { count : 0, flag : true } // action : An object to store the different types of actions the reducer can perform // E.g. action = { type: ‘INCREMENT’ }, action = { type: ‘DECREMENT’ } function counterReducer(count, action) { // Prefer switch case to make the code concise, because the number of actions can be many switch (action.type) { case "INCREASE": return { ...state, count: state.count + 1 }; // Copy previous state and override the specified one case "DECREMENT": return { ...state, count: state.count - 1 }; default: return state; } } // Using the Reducer counterReducer({ count : 0 }, { type: ‘INCREMENT’ }) // 1 counterReducer({ count : 1 }, { type: ‘DECREMENT’ }) // 0
-
We can use payload within the action object, to provide the new information for the states. Here’s an exmaple:
const initialState = { name: "Mark", email: "[email protected]", }; function userReducer(state, action) { switch (action.type) { case "CHANGE_NAME": return { ...state, name: action.payload.name }; case "CHANGE_EMAIL": return { ...state, email: action.payload.email }; default: return state; } } const action = { type: "CHANGE_EMAIL", payload: { email: "[email protected]" }, }; userReducer(initialState, action); // {name: "Mark", email: "mark@compuserve.com"}
- Action Types are saved as variables to prevent typos.
- It is a standard and is widely accepted by the community.
// Basic Action Names
const BUY_CAKE = "BUY_CAKE";
const BUY_ICECREAM = "BUY_ICECREAM";
- You can also save them in a JS file and export them as Named Exports
- These exported variables are mainly imported in the files where reducer function and action creators ares stored.
// Async Action Names
export const FETCH_USERS_REQUEST = "FETCH_USERS_REQUEST";
export const FETCH_USERS_SUCCESS = "FETCH_USERS_SUCCESS";
export const FETCH_USERS_FAILURE = "FETCH_USERS_FAILURE";
- Action Creators are the functions that returns an object containing the action type and payload.
// Basic Action Creator
function buyCake() {
return {
type: BUY_CAKE,
};
}
// Action Creator with Payload
export const fetchUsersSuccess = (users) => {
return {
type: FETCH_USERS_SUCCESS,
payload: users,
};
};
-
Reducer function uses two arguments,
state
andaction
. For state argument a default value is provided known asinitialState
. -
It stores the state of application when Web App is initialized.
// Sync Initial State
const initialState = {
numOfCakes: 10,
numOfIceCreams: 20,
};
// Async Initial State
const initialState = {
loading: false,
users: [],
error: "",
};
-
As soon as the action was dispatched, the state is updated.
-
Example,
buyCake()
,buyIceCream()
const reducer = (state = initialState, action) => {
switch (action.type) {
case "BUY_CAKE":
return {
...state, // Make a copy of all the elements of state
numOfCakes: state.numOfCakes - 1, // Append Changes to this state
};
case "BUY_ICECREAM":
return {
...state, // Make a copy of all the elements of state
numOfIceCreams: state.numOfIceCreams - 1, // Append Changes to this state
};
default:
return state;
}
};
- We wait for a task to be completed before dispatching our actions.
- Example, Asynchronous API Calls to fetch data from an end point and use that data in your application.
const reducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USERS_REQUEST:
return {
...state,
loading: true,
};
case FETCH_USERS_SUCCESS:
return {
...state,
loading: false,
users: action.payload,
error: "",
};
case FETCH_USERS_FAILURE:
return {
...state,
loading: false,
error: action.payload,
};
default:
return state;
}
};
// For Async Reducers we create a special Action Creator which uses thunk Middleware
// This Action Creator dispatches other action creator.
// Thus we'll need to dispatch only one (below mentioned) Action Creator
const fetchUsers = () => {
return function (dispatch) {
dispatch(fetchUsersRequest());
axios
.get("https://jsonplaceholder.typicode.com/users")
.then((response) => {
// response.data is the array of the users
const users = response.data.map((user) => user.id);
dispatch(fetchUsersSuccess(users));
})
.catch((error) => {
// error.message is the error description
dispatch(fetchUsersFailure(error.message));
});
};
};
-
Middlewares are like extensions to Redux.
-
Some Popular Middlewares are:
redux-logger
- Used to Log Redux State Updatesredux-thunk
- Used to Handle Async Operations
const redux = require("redux");
const createStore = redux.createStore;
const store = createStore(reducer);
console.log("State", store.getState());
store.subscribe(() => console.log("Updated state", store.getState()));
// Using Action Creator (Convention)
store.dispatch(buyCake());
store.dispatch(fetchUsersSuccess(users));
// Directly Passing Object (Understanding)
store.dispatch({
type: BUY_CAKE,
});
store.dispatch({
type: FETCH_USERS_SUCCESS,
payload: users,
});
- It is possible to combine multiple Reducers to one reducer known as
rootReducer
.
const combineReducers = redux.combineReducers;
const cakeReducer = (state = initialCakeState, action) => {
...
}
const iceCreamReducer = (state = initialIceCreamState, action) => {
...
}
const rootReducer = combineReducers({
cake: cakeReducer,
iceCream: iceCreamReducer
})
// Don't forget to update the variable inside createStore
const store = createStore(rootReducer)
- Middlewares are added while creating the Redux Store.
// Import the function from Redux
const applyMiddleware = redux.applyMiddleware;
// Create a variable of your Middleware
const reduxLogger = require("redux-logger");
const logger = reduxLogger.logger(); // For Logger
const thunk = require("redux-thunk").default; // For thunk
// Applying Middleware
const store = createStore(rootReducer, applyMiddleware(logger, thunk));
- You can unsubscribe to the store when all the dispatch actions are completed.
const unsubscribe = store.subscribe(() =>
console.log("Updated state", store.getState())
);
unsubscribe();
yarn add react-redux
React-Redux-Project
├── public
| └── favicon.ico
├── src
| ├── components
| | ├── CakeContainer.js
| | ├── IceCreamContainer.js
| | └── UserContainer.js
| ├── redux
| | ├── cake
| | | ├── cakeActions.js
| | | ├── cakeReducer.js
| | | └── cakeTypes.js
| | ├── iceCream
| | | ├── iceCreamActions.js
| | | ├── iceCreamReducer.js
| | | └── iceCreamTypes.js
| | |── user
| | | ├── userActions.js
| | | ├── userReducer.js
| | | └── userTypes.js
| | ├── rootReducer.js
| | ├── store.js
| | └── index.js
| ├── index.js
| └── App.js
├── README.md
├── package.json
└── yarn.lock
The components directory contains all the React Components.
The redux folder has following files and folders:
-
cake/
,iceCream/
,user/
(Reducer folder) -
index.js
(Only exports the Action Creators) -
rootReducer.js
(Combines all the Reducers) -
store.js
(Creates a Redux Store to be used globally)
Each Reducer will have three files. For E.g. User Folder -
└── user
├── userActions.js (Action Creators)
├── userReducer.js (Reducer Function)
└── userTypes.js (Action Types)
- This file only stores the names of Actions as strings.
- It prevents typos and allows easy way to modify, import and export the Actions.
export const FETCH_USERS_REQUEST = "FETCH_USERS_REQUEST";
export const FETCH_USERS_SUCCESS = "FETCH_USERS_SUCCESS";
export const FETCH_USERS_FAILURE = "FETCH_USERS_FAILURE";
- This file will store the Action Creators.
- Action Creators are the functions returning an action as an object.
- For Async processes like fetching data from api is handled by an action creator which dispatches other action creators.
import {
FETCH_USERS_REQUEST,
FETCH_USERS_SUCCESS,
FETCH_USERS_FAILURE,
} from "./userTypes";
import axios from "axios";
export const fetchUsersRequest = () => {
return {
type: FETCH_USERS_REQUEST,
};
};
export const fetchUsersSuccess = (users) => {
return {
type: FETCH_USERS_SUCCESS,
payload: users,
};
};
export const fetchUsersFailure = (error) => {
return {
type: FETCH_USERS_FAILURE,
payload: error,
};
};
// Async Action Creator
export const fetchUsers = () => {
return (dispatch) => {
dispatch(fetchUsersRequest);
axios
.get("https://jsonplaceholder.typicode.com/users")
.then((response) => {
const users = response.data;
dispatch(fetchUsersSuccess(users));
})
.catch((error) => {
const errorMessage = error.message;
dispatch(fetchUsersFailure(errorMessage));
});
};
};
- The Reducer file will contain the
initialState
object and the reducer function. - This reducer function will then get imported into a
rootreducer.js
file.
import {
FETCH_USERS_REQUEST,
FETCH_USERS_SUCCESS,
FETCH_USERS_FAILURE,
} from "./userTypes";
const initialState = {
loading: false,
users: [],
error: "",
};
const userReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USERS_REQUEST:
return {
...state,
loading: true,
};
case FETCH_USERS_SUCCESS:
return {
...state,
loading: false,
users: action.payload,
error: "",
};
case FETCH_USERS_FAILURE:
return {
...state,
loading: false,
users: [],
error: action.payload,
};
default:
return state;
}
};
export default userReducer;
The other redux files are structured as follows.
├── redux
| ├── cake
| | ├── cakeActions.js
| | ├── cakeReducer.js
| | └── cakeTypes.js
| ├── iceCream
| | ├── iceCreamActions.js
| | ├── iceCreamReducer.js
| | └── iceCreamTypes.js
| |── user
| | ├── userActions.js
| | ├── userReducer.js
| | └── userTypes.js
| ├── index.js
| ├── rootReducer.js
| └─── store.js
- The first three folders contain different reducers and their supporting files.
- The three other files are used in combining reducers, creating Redux Store and a file used to export action creators.
- This file combines all the Reducers inside a project.
- In case you have combined multiple reducres, make sure to edit the new name changes in all the Components.
- Initially when we have one reducer we can receive the state like
state.numOfCakes
but when combining multiple reducers and using a root reducer we retrieve states likestate.cake.numOfCakes
. - The syntax will be like
state.{name-of-reducer}.{name-of-state}
.
// Combines all the Reducers
import { combineReducers } from "redux";
import cakeReducer from "./cake/cakeReducer";
import iceCreamReducer from "./iceCream/iceCreamReducer";
import userReducer from "./user/userReducer";
const rootReducer = combineReducers({
cake: cakeReducer,
iceCream: iceCreamReducer,
user: userReducer,
});
export default rootReducer;
- This file creates a Redux Store for your whole project.
- You can add the middlewares inside this file only.
- You can also add
composeWithDevTools
, to make Redux Devtools extension work with the Web App.
// Creates a Redux Store to be used globally
import { createStore, applyMiddleware } from "redux";
import logger from "redux-logger";
import thunk from "redux-thunk";
import rootReducer from "./rootReducer";
import { composeWithDevTools } from "redux-devtools-extension";
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(logger, thunk))
);
export default store;
- This file is used to export the Action Creators, so that we can import them inside any component.
- You can use
*
to export all the Action Creators inside the file.
// Only exports the Action Creators
export { buyCake } from "./cake/cakeActions";
export { buyIceCream } from "./iceCream/iceCreamActions";
export * from "./user/userActions";
Now, when you're done with all the reducers and creating the store, it's time to integrate it with the React Web App.
React-Redux-Project
├── public
| └── ...
├── src
| ├── components/
| | └── ...
| ├── redux/
| | └── ...
| ├── App.js
| └── index.js
├── package.json
└── README.md
We have this global App.js
file where we import all our components. You can add the Redux store provider here to make the Redux store work anywhere inside the Web App.
- The global Project file where all the components are imported.
import React from "react";
import { Provider } from "react-redux";
import store from "./redux/store";
import "./App.css";
import CakeContainer from "./components/CakeContainer";
import ItemContainer from "./components/ItemContainer";
import UserContainer from "./components/UserContainer";
function App() {
// Makes the Redux store available to the connect() calls in the component hierarchy below.
return (
<Provider store={store}>
<div className="App">
{/* <CakeContainer /> */}
{/* <ItemContainer cake /> */}
{/* <ItemContainer /> */}
<UserContainer />
</div>
</Provider>
);
}
export default App;
The below code shows how to connect the component with Redux
-
It requires creating two functions
matchStateToProps
andmatchDispatchToProps
. -
We then connect the component with these two functions, so that we can receive the state and dispatch function inside the component as a prop.
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { fetchUsers } from "../redux";
const UserContainer = ({ userData, fetchUsers }) => {
useEffect(() => {
fetchUsers();
}, []);
return userData.loading ? (
<h2>Loading...</h2>
) : userData.error ? (
<h2>{userData.error}</h2>
) : (
<div>
<h2>User List</h2>
<div>
{userData &&
userData.users &&
userData.users.map((user) => <p>{user.name}</p>)}
</div>
</div>
);
};
// Global state created by Redux
const mapStateToProps = (state) => {
return {
userData: state.user,
};
};
// Dispatch function by Redux for activating action creators.
const mapDispatchToProps = (dispatch) => {
return {
fetchUsers: () => dispatch(fetchUsers()),
};
};
// The connect function that binds everything together
export default connect(mapStateToProps, mapDispatchToProps)(UserContainer);
We can also send the payload to the Action Creators by asking information from the user.
import { BUY_CAKE } from "./cakeTypes";
// Pass 1 as default to add the ability to dispatch without argument
export const buyCake = (number = 1) => {
return {
type: BUY_CAKE,
payload: number,
};
};
import React, { useState } from "react";
import { connect } from "react-redux";
import { buyCake } from "../redux";
const NewCakeContainer = (props) => {
const [number, setNumber] = useState(1);
return (
<div>
<h2>Number of Cakes - {props.numOfCakes}</h2>
<input
type="text"
value={number}
onChange={(event) => setNumber(parseInt(event.target.value))}
/>
<button onClick={() => props.buyCake(number)}>Buy {number} Cake</button>
</div>
);
};
const mapStateToProps = (state) => {
return {
numOfCakes: state.cake.numOfCakes,
};
};
const mapDispatchToProps = (dispatch) => {
return {
buyCake: (number) => dispatch(buyCake(number)),
};
};
// connect() connects the React Component to Redux Store
export default connect(mapStateToProps, mapDispatchToProps)(NewCakeContainer);
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { buyCake } from "../redux";
/*
The React Redux Docs say to use React Redux Hooks over the connect(), but there are a few Usage Warnings.
Usage Warnings - https://react-redux.js.org/api/hooks#usage-warnings
*/
const HooksCakeContainer = () => {
// Below function works similar to the mapStateToProps() in CakeContainer.js
// const numOfCakes = useSelector(state => state.numOfCakes)
// Multiple Reducers
const numOfCakes = useSelector((state) => state.cake.numOfCakes);
// Dispatch function takes in Action Creator
const dispatch = useDispatch();
return (
<div>
<h2>Num of Cakes - {numOfCakes}</h2>
<button onClick={() => dispatch(buyCake())}>Buy Cake</button>
</div>
);
};
export default HooksCakeContainer;
- In the below example we'll connect the Cake and Ice Cream components and actions into one component called Item.
- We use
ownProps
argument inmapDispatchToProps
to differentiate between which state to change. - The benefit of using this method is that we'll have to create single component instead of creating different components. For example, using item instead of Cake and Ice Cream.
import React from "react";
import { connect } from "react-redux";
import { buyCake, buyIceCream } from "../redux";
const ItemContainer = (props) => {
return (
<div>
<h2>Item - {props.item}</h2>
<button onClick={props.buyItem}>Buy Items</button>
</div>
);
};
const mapStateToProps = (state, ownProps) => {
const itemState = ownProps.cake
? state.cake.numOfCakes
: state.iceCream.numOfIceCreams;
return {
item: itemState,
};
};
const mapDispatchToProps = (dispatch, ownProps) => {
const dispatchFunction = ownProps.cake
? () => dispatch(buyCake())
: () => dispatch(buyIceCream());
return {
buyItem: dispatchFunction,
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ItemContainer);
To use it we'll have to pass a prop while adding the Component in App.js
// When we want to alter the state of Cake only
<ItemContainer cake>
// When we want to alter the state of iceCream only
<ItemContainer iceCream>