Learn Redux and Redux with React in a simple way
Shivam Jha
AI Team Lead | NLP | Computer Vision | Machine Learning | GCP | DevOps | Full Stack Web Development | Full Stack Mobile Application Development | Prompt Engineer | LLM | Langchain
You don't have to go anywhere or spend your time learning Redux on the internet or from books here I include everything you need to learn Redux and then I also include three methods to connect Redux to React all in one place.
In a Redux application
The state is usually represented as a JavaScript object. Each property or attribute or key of
the object describes a substate of the application
A simple blog application state or object could consist of an array of posts (which
are written by the user and contain some text):
{
posts: [
{ user: 'dan', text: 'Hello World!' },
{ user: 'des', text: 'Welcome to the blog' }
]
}
Imagine that we want to add a category string to posts later—we can simply add this
property or key to the objects in the posts array:
{
posts: [
{ user: 'dan', category: 'hello', text: 'Hello World!' },
{ user: 'des', category: 'welcome', text: 'Welcome to the blog' }
]
}
let's say we want to implement filtering posts by category; we could extend our
state object with a filter property that stores the category as a string:
{
posts: [
{ user: 'dan', category: 'hello', text: 'Hello World!' },
{ user: 'des', category: 'welcome', text: 'Welcome to the blog' }
],
filter: 'hello'
}
We can reconstruct the whole application state from this object. Being able to do this
is one of the things that makes Redux so awesome.
We also need a way to change the state.
In Redux, we never modify the state directly.
Only actions can change the state
Redux actions are simply JavaScript objects, with a type property that specifies the name of the action.
We want to create a new post in our blog, we could use an action like this:
{ type: 'CREATE_POST', user: 'dan', text: 'New post' }
We could define another action for setting the filter:
{ type: 'SET_FILTER', filter: 'hello' }
These action objects can be passed to Redux, resulting in a new state being
calculated from the current state and the action. This process is called dispatching
an action.
The state is updated through special functions called reducers. Reducers contain the state changing logic of our application.
newState = reducer(state, action)
A reducer function takes the current state object and an action object as arguments.
The reducer parses the action object, specifically, the action.type. Depending on the
action type, the reducer function either returns a new state, or it simply returns the
current state (if the action type is not handled in this reducer).
A reducer function takes the current state and an action argument. For the state argument, we set a default value, which is what the initial state is going to be just like use a state having a first initial value
In our example application, the initial state is an empty array of posts, so we can define the reducer function, as follows:
function postsReducer (state = [], action) {
The most common way to handle
actions in Redux is using a switch statement on action.type. That way, we can have
separate cases for all the different action types that the reducer function is going to take care of:
switch (action.type) {
We handle the CREATE_POST action we defined earlier using
Array.concat to add the new post object to the state (an array of posts):
case 'CREATE_POST':
return state.concat([{ user: action.user, text: action.text }])
For all other action types, we simply return the current state:
default:
return state
}
If you do not return the current state for unhandled action types, your state will become undefined
To access the application state, Redux provides a .getState() function on the store object. You can view the full state, as follows:
console.log(store.getState())
The output of the preceding code will be the application state. In our example
application, the output would be the post array we defined earlier:
[
{ user: 'dan', text: 'Hello World!' },
{ user: 'des', text: 'Welcome to the blog' }
]
Given the same input, pure functions always return the same output. Because reducer
functions are pure, given the same state and action, they are always going to return the same new state
The following code defines an impure function, because subsequent calls with the
same input result in different output:
var i = 0
function impureCount () {
i += 1
return i
}
console.log(impureCount()) // prints 1
console.log(impureCount()) // prints 2
As you can see, we are accessing a variable outside of the function which is what makes the function impure.
We could make the function pure by specifying i as an argument:
function pureCount (i) {
return i + 1
}
console.log(pureCount(0)) // prints 1
console.log(pureCount(1)) // prints 2
Reducers in Redux are always pure functions. They take the previous state and an
action as arguments and return a new state object. The new part is important here. We
never modify the passed state directly, because that would make the function impure.
We always need to create a new state object based on the old state.
In our reducer function, we used Array.concat to create a new array from the old state
array, adding the new post at the end:
function postsReducer (state = [], action) {
switch (action.type) {
case 'CREATE_POST':
return state.concat([{ user: action.user, text: action.text }])
default:
return state
}
}
You might think that such a reducer function will become very complicated, as it
deals with the whole application state. Usually, you start out with a single simple
reducer. As your application grows, you can split it up into multiple smaller
reducers, each reducer dealing with a specific part of the application state. Because
reducers are just JavaScript function
Instead of directly modifying the previous state, a pure reducer function copies data from the
previous state to create a new state object
Let's define a simple action, in the src/index.js file:
const createPost = { type: 'CREATE_POST', user: 'dan', text: 'New post' }
console.log(createPost)
In Redux projects, we usually store the type property of the action in a constant, as follows:
const CREATE_POST = 'CREATE_POST'
const createPost = { type: CREATE_POST, user: 'dan', text: 'New post' }
console.log(createPost)
We can put the action types in a separate actionTypes.js file and export them
1. Create a new actionTypes.js file and define and export the action type there:
export const CREATE_POST = 'CREATE_POST'
2. Then, we can import the action types that we need in other files. Replace the
src/index.js file with the following code:
import { CREATE_POST } from './actionTypes'
Action creators are functions that return actions. They can be thought of as an action factory.
We tell the function what we want to do, and with which data We could replace our
createPost action with a dynamic createPost(user, text) action creator:
function createPost (user, text) {
return {
type: CREATE_POST,
user: user,
text: text
}
}
console.log(createPost('dan', 'New post'))
We can reuse this function to create similar actions
We will learn how to wrap action creators with the Redux dispatch() function.
We could write the createPost(user, text) function in arrow function
syntax, as follows:
const createPost = (user, text) => {
return {
type: CREATE_POST,
user: user,
text: text
}
}
Additionally, ES2015 allows us to shorten property definitions where the object key
and constant name are the same (for example, user: user):
const createPost = (user, text) => {
return { type: CREATE_POST, user, text }
}
And we could return the object directly, by wrapping it with ( ) brackets:
const createPost = (user, text) => ({ type: CREATE_POST, user, text })
you can import/export action creators the same way as
action types. Perform the following steps:
1. Create a new actions.js file. First, we import the CREATE_POST action type:
import { CREATE_POST } from './actionTypes'
2. Then we define and export the createPost action creator: returning js object
export const createPost = (user, text) => {
return { type: CREATE_POST, user, text }
}
3. Finally, we can import and use the createPost action creator in the src/index.js
file. Replace the whole file with the following code:
import { createPost } from './actions'
console.log(createPost('dan', 'New post'))
{
posts: [
{ user: 'dan', category: 'hello', text: 'Hello World!' },
{ user: 'des', category: 'welcome', text: 'Welcome to the blog' }
],
filter: 'hello'
}
As you can see, we have two separate sub-states in the main state object: posts and
filter. This is a good hint for making two reducers: a postsReducer to handle the
posts sub-state, and a filterReducer to handle the filter sub-state
There are no generic rules on how to split up reducers and your application state. It
depends on your projects' requirements and the existing structure.
Edit src/actionTypes.js and define and export the following action types:
export const EDIT_POST = 'EDIT_POST'
export const SET_FILTER = 'SET_FILTER'
ACTION CREATOR FILE
import { CREATE_POST, EDIT_POST, SET_FILTER } from './actionTypes'
export const createPost = (user, text) => {
return { type: CREATE_POST, user, text }
}
export const editPost = (id, text) => {
return { type: EDIT_POST, id, text }
}
export const setFilter = (filter) => {
return { type: SET_FILTER, filter }
}
Then import the additional action creators we defined in the src/index.js file:
import { createPost, editPost, setFilter } from './actions'
We will put our reducers into a separate file:
1. Create a new reducers.js file and import the action types we defined earlier:
import { CREATE_POST, EDIT_POST, SET_FILTER } from './actionTypes'
2. Then, define and export the reducer function:
export function postsReducer (state = [], action) {
We will learn how to split this reducers.js file into multiple files
and make a reducers/ folder
In the Redux world, it is best practice to use switch statements to handle multiple
action types. Feel free to use if/else statements instead, especially when a reducer
handles only a single action type:
case CREATE_POST: {
return ...
}
case EDIT_POST: {
return ...
}
Note how we don't use a string ('CREATE_POST'), but the action
type/constant (CREATE_POST).
3. Defining the default branch is very important: if you do not return the current
state for unhandled action types, your state will become undefined:
default:
return state
}
}
Using destructuring and the rest operator to parse the action
We can manually pull out properties from the action object, as follows:
case CREATE_POST: {
const type = action.type
...
}
Instead of that, we can use destructuring, which would make the code look like this:
case CREATE_POST: {
const { type } = action
...
}
const { type, user = 'anon' } = action
// instead of
const type = action.type
const user = action.user || 'anon'
It is also possible to rename properties, as follows:
const { type: actionType } = action
// instead of
const actionType = action.type
The preceding code will store action.type in the actionType constant.
You can collect the rest of the properties
into another object. This can be done with the rest operator. It works as follows:
const { type, ...post } = action
In this way, we separate type and post
We can use the rest operator instead of manually collecting the rest of the properties:
const type = action.type
const post = {
user: action.user,
category: action.category,
text: action.text
}
For example, if we want to add a title to posts later, we could
simply add a title property to the action and it would already be in the post object
when using the rest operator we can use that knowledge to parse the CREATE_POST action type:
1. First, we will pull out the data that we need from the action. In this case, we
want to store everything—except for the type property—in a post object:
case CREATE_POST: {
const { type, ...post } = action
2. Then, we will insert this post object into our array and return the new state.
We use something similar to the rest operator—the spread
operator, which is basically the reverse of the rest operator. Instead of
collecting all properties of an object/elements of an array, it spreads them out:
return [ ...state, post ]
}
Using the spread operator achieves the same thing as using Array.prototype.concat(),
which merges two arrays and returns a new one:
like
return state.concat([ post ])
we will handle the EDIT_POST action type:
1. We start by parsing the action object:
case EDIT_POST: {
const { type, id, ...newPost } = action
This time, we will pull out the type and id properties, because we will
need the id later, and it should not be part of the post object.
We use the Array.prototype.map() function to return a new state array. If
you do not know this function, it has the following signature:
const newArr = arr.map(
(elem, index) => { ... }
)
The map function executes the callback function on each element and creates a new array
from the return values of that callback function. Basically, map lets us apply a function to all elements in an array.
perform some action defined in function on each element of the array and return an updated array
If we wanted to increase all numbers in an array by one, we
could use the map function, as follows:
const numbers = [1, 2, 3]
const newNumbers = numbers.map(
number => number + 1
)
console.log(newNumbers) // outputs: [2, 3, 4]
We can use map to edit a single post in the array, by going through all posts and
returning the original post object, except for the one element that matches the
index:
return state.map((oldPost, index) =>
3. First, we will make sure that the id value equals the index of the array because
we want to edit only one of the posts:
action.id === index which matches with action.id
Now, we can overwrite the oldPost properties with the newPost properties using
the spread operator as follows:
? { ...oldPost, ...newPost }
: oldPost
)
}
When the index value matches the id specified in the action, the post object will be
overwritten with all other properties from the action. When the index value doesn't
match, we simply return the oldPost.
we have not defined index in state but map function giving us index
Imagine that we have the following posts:
posts: [
{ user: 'dan', category: 'hello', text: 'Hello World!' },
{ user: 'des', category: 'welcome', text: 'Welcome to the blog' }
]
We want to edit the first post, so we dispatch the following action:
{ type: 'EDIT_POST', id: 0, text: 'Hi World!' }
Our reducer would match the post with the 0 index in the array, which is the first
one. Then, we have the following values:
const oldPost = { user: 'dan', category: 'hello', text: 'Hello World!' }
const newPost = { text: 'Hi World!' }
We use the spread operator to combine the objects:
{ ...oldPost, ...newPost }
This means that it will first spread out all properties from the oldPost object:
{ user: 'dan', category: 'hello', text: 'Hello World!', ...newPost }
Now, all properties from the newPost object will get spread out, overwriting some of
the oldPost properties, and resulting in a new object:
{ user: 'dan', category: 'hello', text: 'Hi World!' }
EXAMPLE
Now that we have written our reducer function, we can test it out by passing data
directly to it. First, we will define the initial state, an empty array of posts:
const initialState = []
Now, we will need to define the action. To do this, we use the previously defined
createPost action creator:
const action = createPost('dan', 'New post')
Finally, we execute the reducer and log the result:
const newState = postsReducer(initialState, action)
console.log(newState)
The preceding code should have the following output:
[
{
"user": "dan",
"text": "New post"
}
Writing the filter reducerThe filterReducer only handles one action type, so an if/else statement can be used.
Define the reducer function in the src/reducers.js file:
function filterReducer (state = 'all', action) {
if (action.type === SET_FILTER) {
return action.filter
} else {
return state
}
}
This reducer function is pretty simple—it sets the filter state to whatever is entered in action.filter.
Here not replacing but returning the same filter that we pass through action just used that trick
Now, all that is left to do is combining these two reducers together to handle the
whole state and all actions of the application:
function appReducer (state = {}, action) {
return {
posts: postsReducer(state.posts, action),
filter: filterReducer(state.filter, action),
}
}
Redux provides a combineReducers helper function, which creates a function that
essentially does the same thing as the appReducer function we defined earlier:
1. Import the combineReducers helper function at the beginning of the src/reducers.js
file:
import { combineReducers } from 'redux'
2. Then call combineReducers with our reducer functions at the end of the
src/reducers.js file. It returns an appReducer function:
const appReducer = combineReducers({
posts: postsReducer,
filter: filterReducer,
})
export default appReducer
Now, you can import the reducer in the src/index.js file, as follows:
import appReducer from './reducers'
filterReducer function that takes care of handling the filter part of our state object. By default, we want to show all posts, so we set the default state to 'all'.
After combining our reducers, we can test them out by calling the appReducer:
1. First, we will initialize the reducers by passing an undefined state:
let state = appReducer(undefined, { type: 'INIT_ACTION' })
console.log('initial state:', state)
The resulting initial state looks as follows:
{
"posts": [],
"filter": "all"
}
Then, we will dispatch some actions by manually passing the current state and
the action object to the appReducer function. We will start with a createPost
action:
state = appReducer(state, createPost('dan', 'test'))
console.log('state after createPost:', state)
The state now looks like this:
{
"posts": [
{
"user": "dan",
"text": "test"
}
],
"filter": "all"
}
Then, we will edit the text of that post:
state = appReducer(state, editPost(0, 'edited post'))
console.log('state after editPost:', state)
This results in only the first post (index 0) getting changed:
{
"posts": [
{
"user": "dan",
"text": "edited post"
}
],
"filter": "all"
}
Finally, we will set the filter, which does not affect the posts array at all:
state = appReducer(state, setFilter('none'))
console.log('state after setFilter:', state)
The final state is as follows:
{
"posts": [
{
"user": "dan",
"text": "edited post"
}
],
"filter": "none"
}
Redux brings actions and reducers together.
It provides a store, which contains the application state and provides some functions to do the following:
Access the application state: store.getState()
Dispatch actions: store.dispatch(action)
Register listeners: store.subscribe(listener)
We will now cover how to use Redux to create a store from our appReducer, how to
listen to changes in the store, and how to dispatch actions to the store:
import { createStore } from 'redux'
it takes the first arguments
the main/root reducer, in our case, appReducer) as the first argument.
An initial state as the second argument--if no initial state is
specified, the store will be initialized by passing undefined to the
reducer
The createStore function returns the Redux store, which provides some
functions to access the state (store.getState()), listen to state changes
(store.subscribe(listener)), and dispatch actions (store.dispatch(action)):
let store = createStore(reducer, initialState)
We use the createStore function to create the store from our previously created
appReducer. Instead of manually calling the reducer, we replace the rest of the
code in src/index.js with the following:
let store = createStore(appReducer)
console.log(store.getState())
{
"posts": [],
"filter": "all"
}
const unsubscribe = store.subscribe(() => {
console.log('state changed:', store.getState())
})
You might have noted that this code does not produce any output yet. The
initialization process of the Redux store does not trigger a call to our subscribed
function. We first need to dispatch an action to see the state change being logged.
Now, there is only one thing left to do—dispatching actions to the store. You can
simply pass action objects to the store.dispatch() function:
store.dispatch({ type: 'CREATE_POST', user: 'dan', text: 'hello world' })
However, it is best practice to use action creators instead, as follows:
store.dispatch(createPost('dan', 'hello world'))
Dispatching an action will result in the state being changed (by executing the
reducer), and thus, the subscribed function being called and give change state as an output
then at the last step
unsubscribe()
Connect redux with react
Put the following code in the src/components/Post.jsx file
import React from 'react'
const Post = ({ user, text }) =>
<span><b>{user}</b> - {text}</span>
export default Post
In the src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import Post from './components/Post.jsx'
ReactDOM.render(
<Post user="dan" text="hello world!" />,
document.getElementById('root')
)
Create a new src/components/PostList.jsx file if we have multiple post then
The PostList component will take a posts array as a property and render multiple
Post components. We will use the map() function like
Here we will need more li tag to display more post when we will pass multiple post list from index.js
import React from 'react'
import Post from './Post.jsx'
const PostList = ({ posts }) =>
<ul>
{posts.map(
(post, i) =>
<li key={i.toString()}>
<Post {...post} />
</li>
)}
</ul>
export default PostList
Replace src/index.js with the following code:
import PostList from './components/PostList.jsx'
const posts = [
{ user: 'dan', text: 'hello world!' },
{ user: 'des', text: 'hi!' }
]
ReactDOM.render(
<PostList posts={posts} />,
document.getElementById('root')
)
Now we will connect redux to react
In this small example, we only need one container component, ConnectedPostList:
import React from 'react'
import PostList from '../components/PostList.jsx'
export default class ConnectedPostList extends React.Component {
constructor (props) {
super(props)
this.state = props.store.getState()
}
We continue with the componentDidMount() life cycle method, where we subscribe
to the store and call this.setState with the new Redux state:
componentDidMount () {
const { store } = this.props
this.unsubscribe = store.subscribe(() =>
this.setState({ ...store.getState() }) you can also console.log but we have to use in react component
)
}
store.subscribe() returns an unsubscribe() function, which can be used to
remove the subscription so,
componentWillUnmount () {
this.unsubscribe()
}
Finally, we write the render() method, which will return our component, passing all state down to it as properties:
render () {
return <PostList {...this.state} /> now displaying all value store in component store that taken from central store using ../components/PostList.jsx
}
}
In the main file src/index.js, we now import everything that we need, including
the ConnectedPostList component:
import { createStore } from 'redux'
import React from 'react'
import ReactDOM from 'react-dom'
import { createPost } from './actions'
import appReducer from './reducers'
import ConnectedPostList from './containers/ConnectedPostList.jsx'
Then, we create the Redux store:
let store = createStore(appReducer)
We dispatch a createPost action, so we can see a post:
store.dispatch(createPost('dan', 'hello world'))
After a second, we dispatch another createPost action:
setTimeout(() => store.dispatch(createPost('des', 'hi!')), 1000)
Finally, passing store as a props from index.js to ConnectedPostList then we render our ConnectedPostList component via ReactDOM:
ReactDOM.render(
<ConnectedPostList store={store} />,
document.getElementById('root')
)
We successfully connected React to Redux, without using bindings!
This is one above method using createStore
Now we will see using another method by using function connect from react-redux using another example
create cakeTypes.js
export const BUY_CAKE = 'BUY_CAKE'
create cakeActions.js
import { BUY_CAKE } from './cakeTypes' it is action creator file
export const buyCake = (number = 1) => {
return {
type: BUY_CAKE,
payload: number
}
}
cakeReducer.js this is reducer.js you can make saparte folder for other reducer in reducer folder
import { BUY_CAKE } from './cakeTypes'
const initialState = {
numOfCakes: 10
}
const cakeReducer = (state = initialState, action) => {
switch (action.type) {
case BUY_CAKE: return {
...state,
numOfCakes: state.numOfCakes - action.payload
}
default: return state
}
}
export default cakeReducer
rootReducer.js here you can combine any number of reducer just like here iceCreamReducer userReducer
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
store.js to apply middleware and redux thunk is an action creator that returns a function to perform asynchronous dispatch if you want then
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import rootReducer from './rootReducer'
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(logger, thunk))
)
export default store
Now we have to use provider to provide central state as a props to other components
App.js
import React from 'react'
import { Provider } from 'react-redux'
import store from './redux/store'
import CakeContainer from './components/CakeContainer'
import HooksCakeContainer from './components/HooksCakeContainer' if you will use hooks we will see later
function App () {
return (
<Provider store={store}>
<div className='App'>
<CakeContainer />
<HooksCakeContainer />
</div>
</Provider>
)
}
export default App
Now final step
CakeContainer.js to display our data from a store in react app here used that function and concepts
1. react-redux provides a function called connect.
2. connect(mapStateToPropsFunc, mapDispatchToPropsFunc)
(Component)
3. mapstatetoProps function name can be anything,
argument- state(Application State)
whenever state changes prop's value will also change
if prop's values changes then our component will re-render
we return an object of props
4. mapDispatchToProps -
argument- dispatch function
dispatch function is used to dispatch an action
action object we get from action creators
return object of props
import React from 'react'
import { connect } from 'react-redux'
import { buyCake } from 'cakeActions'
function CakeContainer (props) {
return (
<div>
<h2>Number of cakes - {props.numOfCakes} </h2>
<button onClick ?={props.buyCake}>Buy Cake</button>
</div>
)
}
const mapStateToProps = state => {
return {
numOfCakes: state.cake.numOfCakes
}
}
const mapDispatchToProps = dispatch => {
return {
buyCake: () => dispatch(buyCake())
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CakeContainer)
Now using hooks third method all will be same only one above file CakeContainer.js will be change
HooksCakeContainer.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { buyCake } from 'cakeActions'
function HooksCakeContainer () {
const numOfCakes = useSelector(state => state.cake.numOfCakes)
const dispatch = useDispatch()
return (
<div>
<h2>Number of cakes - {numOfCakes} </h2>
<button onClick ?={() => dispatch(buyCake())}>Buy Cake</button>
</div>
)
}
export default HooksCakeContainer
Here useSelector hook can be used to select a part from state to use component here used in a place of mapStateToProps
useDispatch() in a place of mapDispatchToProps
Sometimes you also use useReducer for global state management
import React, { useReducer } from "react";
// Defining the initial state and the reducer
const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case "add":
return state + 1;
case "subtract":
return state - 1;
case "reset":
return 0;
default:
throw new Error("Unexpected action");
}
};
const App = () => {
// Initialising useReducer hook
const [count, dispatch] = useReducer(reducer, initialState);
return (
<div>
<h2>{count}</h2>
<button onClick ?={() => dispatch("add")}>
add
</button>
<button onClick ?={() => dispatch("subtract")}>
subtract
</button>
<button onClick ?={() => dispatch("reset")}>
reset
</button>
</div>
);
};
export default App;