Redux is one of the tools that can be challenging for a beginner to get started using it. from its fancy terms like reducers,
dispatch
, payload
, to a lot of packages built on top of it like Redux-saga
, Redux-thunk
, Redux-promise
... one may struggle even to find where to start and easily get lost into these Redux Mumbo jumbos.
In this guide, we will go through the basics of Redux from a beginner's perspective using the simple plain English language. By the end of the article, you will have a strong understanding of Redux basics, and we will use our new Redux knowledge to build a simple banking application.
Note: This article will use the traditional way of using Redux (without using redux-toolkit
). I intentionally made it this way to focus on explaining redux concepts with less confusing code.
I plan to make this a series and in the following articles, we will integrate redux-toolkit in our code and see precisely the problems that the redux-toolkit
was created to solve.
Don't worry if you don't even know what redux
or redux-toolkit
we will cover everything from scratch
Prerequisites
- We will be building everything from scratch; you only need VS code and node installed on your machine
Let's start by explaining some of the Redux terminologies
1. What exactly is Redux
When you visit the official Redux Website, you see this simple definition ==> Redux:
A Predictable State Container for JS Apps
But what does this even mean? How did they manage to make 7 words so difficult to understand?
First, what is the application state?
Generally, the state of your application is the situation or environment in which your app is running, and this state usually changes. For example, suppose you have a website like Facebook where users can log in and log out from their accounts,
when someone just lands on the website, we can say that the application is in a state where no user is logged in, but as soon as they enter their credentials and click the login button, the state changes, and now the app is the state where someone is logged in.
Not clear yet? Let's take another example, let's say on a website users can visit and decide to use the dark or light mode by toggling the theme slider. As soon as they switch modes everything will change in the app and the selected mode will be applied.
We can say that the app was in the state of light mode and is now in the state of dark mode.
As explained in the above examples our application behaves differently according to the current state and we need to track the state of the application to build modern information like is the user logged in?, is the page loading?, the key that the user pressed...
That's where Redux
comes into the picture as a container of application state.
Basically in Redux, the state of our application is just a long JavaScript object containing all the information describing every change we need to keep track of.
For example, It can look like the following (note that the examples are simplified for the sake of clarity
let state = {
userLoggedIn: true,
mode: "dark",
}
We can describe the above state by saying that the user is logged in and chose to use dark mode. When using Redux this object will be managed by Redux and it will track all changes we can access the updates from Redux whenever we want.
Back to the Redux definition
so the definition that Redux is "A Predictable State Container for JS Apps" means that Redux is so good at managing your application state that whenever the state in your JS application expects that it is recorded by Redux
Now that we have covered the Redux definition let's take a look at some of its concepts
2. Redux actions
Redux's actions are much similar to real-life actions, they describe how to do something. like the way, you can have an action of Reading a book that's the same with Redux, except that in Redux we are dealing with the application state.
As we saw that we constantly need to change our application state according to different conditions, we need a way to tell Redux how to change the state, and that's where we use actions.
In Redux, an action is a simple JavaScript object that explains the action to perform on our state in order to manipulate it.
for example, an action can look like this
const action1 = {
type: "DO_SOMETHING"
}
- Redux actions will always have a property of
type
which describes what to do. and this property is compulsory. Note that by convention actions type is written in capital letters separated by underscores (also known as capital snake casing) - Redux action can optionally have a property called
payload
which can be anything that gives more detail about how to do the action but this field is optional.
Let's explain Redux actions more using an example. Let's say in real life you want to go to "Simba supermarket" to buy "10 red apples", we can describe this as a Redux action like the Following
const action = {
type: 'BUY_APPLES',
payload: {
shop: 'Simba supermarket',
type: 'red',
number: 10,
}
};
In this example, our action is just an object containing the type describing what we want (to buy apples) and in our case, the payload is another object containing additional information about where to buy apples which type, and how much to buy.
For a more practical example let's say we have a ToDo app and in the app, we have a state of all TODOS, you would want to create an action that will add a new TODO to the state and the action could be like the following
const action = {
type: "ADD_TODO",
payload: "Walk the dog" // payload contains the actual task to do
}
- Note how the payload can also be just a string as in the current I don't have additional information to explain this action
Redux action creators
In Redux as the name says action creators
are functions that create actions. But we already created action in the previous why would we need to use action creators?
Generally, we use functions when we don't want to repeat the same code over again. In the above buying apples example what if we wanted to buy 7 green apples from a different shop? The action is still the same (buying apple) therefore we don't need to create brand new action we can just use a function that takes input and return appropriate action and can be reused whenever needed.
We can use something like
const createAction = (shopName, appleType, appleNumber) => {
return {
type: 'BUY_APPLES',
payload: {
shop: shopName,
type: appleType,
number: appleNumber,
},
};
};
Action creators are functions that just return the action object and they can be given different arguments to provide new actions dynamically
3. Redux Reducers
So we now have the stat, and we have an action we want to do to change the state how exactly do we tell redux to do the action and actually change the state? that's where "reducers" come in.
Redux uses fancy names so what in the world is a reducer? By definition from the documentation
A Reducer is a pure function that takes the state of an application and action as arguments and returns a new state
Pure functions???
Pure functions are functions that will always return the same output whenever they are given the same input.
But isn't that what all functions do? returning the same results? well..., not really
consider these two functions
const functionA = (number)=>{
const sum = number + 2;
return sum;
};
let x = 2 // keep attention here
const functionB = (number)=>{
const sum = number + x;
return sum;
}
these two functions may look like they are doing the same but functionA
is a pure function while functionB
is an impure function. This is because functionA
will always return the sum when the same number is passed
On the other hand but functionB
is depending on variable x
and if this value is changed functionB
will return a different sum. Another important behaviour of a reducer is that it should not change the values that are passed to it as input
There is more to pure functions, I recommend that you read these to articles to understand further article1 article2
Back to the definition of a Reducer
A Reducer is a pure function that takes the state of an application and action as arguments and returns a new state
A reducer is just a function that will take the initial state and the action we want and it will perform the action we want, and return an updated state.
A reducer need to be pure so that it will always walk in a consistent way.
A reducer look something like
const reducer = (state, action)=>{
// do action ...
return newState
}
let's use an example of a counter app that stores the value of the counter and we can have an action to increment or decrement the number by one
We can have 0 as our initial value of the counter
const state = {
count: 0
}
we can have our actions like
const incrementAction = {
type: 'INCREMENT'
}
const decrementAction = {
type: 'INCREMENT'
}
Let's now create a reducer that will take a state and an action to return a new state. When we pass the increment action we will increase the current state by 1 and when we pass the decrement action we decrement it by 1.
We will use switch statements to check which action we have and return the new value accordingly
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT': {
const newState = { ...state };
newState.value = state.value + 1;
return newState;
}
case 'DECREMENT': {
const newState = { ...state };
newState.value = state.value - 1;
return newState;
}
}
};
Let's break the code line by line
const reducer = (state, action)
: We are creating a reducer function that takes the initial state action object as we saw from the definitionswitch (action.type)
As we have two actions we are using the switch statement to check the type of the action. You can also useif-else
statements if you want to.const newState = { ...state }
: this is the most important part a reducer is a pure function and will NEVER mutate the state passed to it as an argument instead we create a new object and copy the previous state using spread operator. we are just creating a new object and copy everything from the state this means thatnewState
andstate
are different objects but they have the same values.newState.value = state.value + 1
: We are changing thevalue
field of newState to be the previous state value incremented or decremented by one according to the action typereturn newState
: we are returning the new state as a reducer should return the new state
the above code can be simplified to be
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, value: state.value + 1 };
case 'DECREMENT':
return { ...state, value: state.value - 1 };
}
};
4. Redux store
Now we have a state, we have actions that describe what to do with the state, and we have a reducer function that implements our action and returns the new state. It looks like we have everything we need we just need better management of all of this code.
Basically, we want that whenever we call the reducer function and update the state, the new state should replace the old state and be our current state so the next time we perform another change we have a track of how the state changed in our app.
To achieve this we need to have everything in the same place which is where the Redux store
comes in.
The Redux store
is like a real-life store that holds records of how the state had changed in your application. For example, when a user login the state changes, and when they log out the state will change again, Redux store will keep a track of these changes so that if anything goes wrong we can see exactly what happened and where it happened.
in Redux to access the store we need to create it first, the store is created using the function createStore()
and this function provides us with functions that we will use to access and manipulate the state
In this guide, we will be focusing on its two functions getState()
and dispatch()
getState()
: when you run this function the store will return the current value of the state and this value will always be the updated state.dispatch()
: In Redux you don't really call the reducer and pass the action directly as we have been doing before, instead you pass the action to thedispatch
function, and the store will have access to the reducer and the state itself and it will do everything for you.
this means that you don't need to worry about what's in the state you just dispatch (send) an action to the store, the store will call the reducer and pass the state and the action you dispatched. the reducer will do its work as we saw earlier and when it returns to the new state the store will automatically update the state to this new state.
It's like the way you go to the bank and you have an action of depositing money to your account the cashier will take your money do her work and add the new amount to your account with no effort done on your end
Don't worry if this you are confused by all of this, let's see all in action as we build our simple banking app
PUTTING EVERYTHING TOGETHER: SIMPLE BANKING APP
Let's use what we learned to build a simple banking app where someone creates accounts, view their balance, deposit, or withdraw money from their account
follow these steps
1. Create a project
create a folder and open it in VS Code initialize a node project by running
npm init -y
- in the
package.json
add atype
field and set it value to"module"
as we will be using imports and export later
2. Install Redux
- install Redux by running the following command
npm install redux
// using yarn
yarn add redux
3. Create a redux store
- create a folder called
redux
and that's where our redux code will be - in the redux folder create a file and name it
store.js
this where we will configure our Redux store - in the 'store.js' file add the following code
import { legacy_createStore as createStore } from 'redux';
const store = createStore();
we are importing createStore from redux and we are creating a new store by invoking that function the createStore
function
4. Create a initial state
Let's have the initial state of our application let's say someone just created a new bank account and their basic information will be our object
- in the
store.js
right before we create thestore
variable we will add a variable of the initial state and will pass our initial state into the store so that it will store it for us the store.js should look like
import { legacy_createStore as createStore } from 'redux';
const initialState = {
accountOwner: 'John Doe',
address: 'Miami',
balance: 0,
};
const store = createStore(initialState);
export default store;
- we are creating an initial state that includes basic information of the owner and their balance is 0$ as they just created a new account and they don't have money yet.
5. Create action using action creator
Remember the actions and action creators we talked about earlier right? actions are object and action creators are function that return these objects
- in the redux folder create file called
actions.js
and we will add our action creators - let's create an action for Depositing money , withdrawing money, and changing address
in your actions.js add the following code
export const depositAction = (amount) => {
return {
type: 'DEPOSIT',
payload: amount,
};
};
export const withdrawAction = (amount) => {
return {
type: 'DEPOSIT',
payload: amount,
};
};
export const changeAdressAction = (newAdress) => {
return {
type: 'CHANGE_ADRESS',
payload: newAdress,
};
};
we are creating action creator functions that just return action with type and the payload the value we passed in
for instance the depositAction
will return an action with type DEPOSIT
and a payload of the amount you passed in.
6. Create a reducer
in the redux folder create a reducer.js
file which will contain our reducer
- in the
reducer.js
add the following code
const reducer = (state, action) => {
switch (action.type) {
case 'DEPOSIT':
return { ...state, balance: state.balance + action.payload };
case 'WITHDRAW':
return { ...state, balance: state.balance - action.payload };
case 'CHANGE_ADRESS':
return { ...state, address: action.payload };
default:
return state;
}
};
export default reducer;
- As always it's important that the reducer doesn't mutate the state passed. We create a new object and copy everything in the previous state and change the field we want to change
- in this case when the action is
DEPOSIT
we will change the balance to add amount in the payload to previous balance. the same thing withWITHDRAW
instead we subtract amount in the payload from previous balance - when the action is
CHANGE_ADRESS
we will only change the address field to the new address from the payload - If the action is not known by default we will do nothing we just return previous state unchanged
7. Pass the reducer to the store
Remember that we don't have to do anything ourselves the redux store will do everything for us therefore we need to provide the reducer to the store.
- back to the
store.js
import the reducer function and pass it to thecreateStore
function.
import { legacy_createStore as createStore } from 'redux';
import reducer from './reducer.js';
const initialState = {
accountOwner: 'John Doe',
address: 'Miami',
balance: 0,
};
const store = createStore(reducer, initialState);
export default store;
- we are importing reducer function from
reducer.js
and pass it to thecreateStore
function along with the initial state we had before Note that the reducer should be passed first ascreateStore
function expects the reducer to be the first argument
That's all configurations we need now lets test how everything works
8. Testing
in the root folder create an index.js
file and import the store and actions from redux folder.
- in the
index.js
add the following code
import {
changeAdressAction,
depositAction,
withdrawAction,
} from './redux/actions.js';
import store from './redux/store.js';
console.log('initialState:');
console.log(store.getState());
//
store.dispatch(depositAction(500));
console.log('New state after deposit:');
console.log(store.getState());
//
store.dispatch(changeAdressAction('Paris'));
console.log('New state after change address');
console.log(store.getState());
//
store.dispatch(withdrawAction(300));
console.log('New state after withdraw');
console.log(store.getState());
- to test everything we are just consoling the state by using
store.getState()
remember thatgetState
returns our current state - we are dispatching actions by using
store.dispatch()
and we pass in the function we want to dispatch after dispatching an action we are consoling the state again to see changes
Run
node index.js
in the terminal and you should see the following output
- you can see that after dispatching an action redux did updated our state
There you have it! you now understands the basics of Redux In the following article in this series we will look into how to use Redux-toolkit
to write cleaner code and integrate Redux in a real redux app that is more interactive.
For reference you can find code mentioned in this article on this github repo
Top comments (1)
nice one