This is part two of my Redux mini-series. You can find the first part here. I highly recommend reading it first if you are new to Redux.
In my first post, we learned conceptually what Redux does and why we needed Redux. Let's jump into the code!
Setup
The repository can be found here. I will go through with the code.
git clone https://github.com/iggredible/basic-redux.git
cd basic-redux
npm i
npm run start
If you want to start from scratch, you can use create-react-app. Also install redux and react-redux.
Code breakdown
I will go over Redux action and reducer. Then I will cover how to connect Redux to our app. Store and initialState will also be covered by the end of the code walkthrough! 👍
Most of our Redux files are inside src/javascripts
. Inside you will see actions/
and reducers/
. Let's go to actions first.
Actions
Inside actions/
, we see two files: index.js
and types.js
. Let's talk about types first.
Types are constants. A Redux action is a JS object. This object tells our reducer what to do with our states. A typical action might look like this:
{
type: CHANGE_BUTTON_COLOR,
color: 'red'
}
or very simple one like this:
{
type: TOGGLE_IS_HIDDEN,
}
Every action needs a type. The convention for type that Redux uses is that it has to be string, all caps, and snake case.
We store our types inside types.js
export const ADD_NOTE = "ADD_NOTE"
export const DELETE_NOTE = "DELETE_NOTE"
You may wonder, "why would I want to go out of my way to create a file full of constants? Why can't I just type the types as I go? "
Valid enough. The reasons are:
- Prevent typos
- Keep track of all available types
- Modularity
When your app grows, your types will grow. It is normal to have hundreds of types in a project and with that, chances of misspelling a word increases. Using a dedicated file for constants reduces the chance of misspelling.
Additionally, if a new developer joins your project few years down the road, that dev can just look at types.js
and get a good idea what functionalities your app can do!
Lastly, when your app grows to have hundreds of types, you can split them for modularity. You can have something like actions/types/customer.js
for all your customer related action types and actions/types/merchandise.js
for all your merchandise related action types.
Now let's go where the actions are (pun intended 🤓)
// actions/index.js
import {ADD_NOTE, DELETE_NOTE} from "./types";
let id = 0;
export const addNote = notes => {
id++;
return {
type: ADD_NOTE,
notes: {...notes, id: id}
}
}
export const deleteNote = id => {
return {
type: DELETE_NOTE,
id
}
}
We have two actions: one to add a note and one to delete a note. If you notice, they both return a plain JS object. Forewarning, it needs to at least have a type
. Actions are set of instructions that will be sent to our reducer.
Think of it like a grocery list. Sometimes my wife would ask me to grab fruits from the store. In this case, she would give me an action that looks like this:
{
type: PICKUP_GROCERY,
items: ['mangoes', 'rice', 'cereal']
}
Remember, an action does not do anything yet. It is simply an instruction. The execution happens in reducer.
When we sends off an action to reducer, in Redux' term, we call it dispatching.
Here we have two actions: on to add a note and one to delete it. In our simple note app, we would give our submit button the addNote
dispatcher and the delete
button next to each note deleteNote
dispatcher.
Let's see how action gets executed in reducer!
Reducer
Inside src/reducers/index.js
, we see:
import {ADD_NOTE, DELETE_NOTE} from "../actions/types";
const initialState = [
{title: "First Note", id: 0}
]
function rootReducer(state = initialState, action){
switch(action.type){
case ADD_NOTE:
return [...state, action.notes]
case DELETE_NOTE:
return state.filter(note => note.id !== action.id)
default:
return state;
}
}
export default rootReducer;
Let's go through it top to bottom.
The first line is self-explanatory:
import {ADD_NOTE, DELETE_NOTE} from "../actions/types";
It imports the constants from types.
const initialState = [
{title: "First Note", id: 0}
]
This is our initial state. Every time we run our app, we see that after page loads, we always have one note called "First Note". This is the initial state. Even after you delete it, if you refresh the page, redux resets, our states go back to initial state, and you'll see "First Note" again.
This is the main functionality of our reducer function:
function rootReducer(state = initialState, action){
switch(action.type){
case ADD_NOTE:
return [...state, action.notes]
case DELETE_NOTE:
return state.filter(note => note.id !== action.id)
default:
return state;
}
}
Our reducer takes two arguments: state and action. As default value, we give it initialState.
Note the switch case:
switch(action.type){
case ADD_NOTE:
return [...state, action.note]
case DELETE_NOTE:
return state.filter(note => note.id !== action.id)
default:
return state;
}
Conventionally, reducers use switch case to decide what to execute depending on the action type it receives.
If we pass it ADD_NOTE
type, it finds a match and returns: [...state, action.note]
.
I am not doing return state.push(action.note)
, but instead [...state, action.note]
. This is important. If I had done .push()
, I would be changing the state stored in redux. We do not want that. Our reducer needs to be a pure function.
A pure function is function that: does not produce side effect and given the same input, will always return the same output. Further explanation is outside the scope of this tutorial, but you can check this and this out!). Just know that your reducer must never change the original state.
Connecting Redux to our React app
Phew, we finished with actions and reducers. We need to connect our Redux to React. Go to src/index.js
:
import React from 'react';
import ReactDOM from 'react-dom';
import App from "./App"
import {createStore} from 'redux';
import {Provider} from 'react-redux';
import rootReducer from './javascripts/reducers'
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
At minimum you need:
- a reducer function (in this case,
rootReducer
) createStore
from redux andProvider
fromreact-redux
, instantiated usingcreateStore()
- Wrap our app with
Provider
👆 andstore
.
That's it! Now our <App />
is connected to redux. Finally, let's make things work.
React + Redux
I am not going to go through each line of code in App.js, but I will touch on the important things:
import {connect} from "react-redux";
...
const App = connect(mapStateToProps, mapDispatchToProps)(ConnectedApp)
export default App;
We need to connect our React component (named ConnectedApp
) to our store. We will use {connect}
from react-redux
library and connect it with mapStateToProps
and mapDispatchToProps
. This App
then gets exported.
You might wonder what do mapStateToProps
and mapDispatchToProps
do 🧐?
const mapStateToProps = state => {
return {
notes: state
}
}
const mapDispatchToProps = dispatch => {
return {
addNote: note => dispatch(addNote(note)),
deleteNote: note => dispatch(deleteNote(note))
}
}
mapStateToProps
and mapDispatchToProps
, as the name suggests, maps our redux states and redux actions to be used as props in our app.
In mapStateToProps
, we receive state
argument - this state
is all our Redux states. In effect, we can now view all our states as notes props! Inside our app, we can see our states with this.props.notes
.
Which is what we did. Inside render, you'll see:
render() {
const { notes } = this.props;
...
If it wasn't mapped in mapStateToProps
, you would get undefined. Our this.props.notes
is now our Redux states! How cool is that? This is how our we access the states.
The same goes with our dispatchToProps. Guess what this does:
const mapDispatchToProps = dispatch => {
return {
addNote: note => dispatch(addNote(note)),
deleteNote: note => dispatch(deleteNote(note))
}
}
Some of you might even guessed it. Let's compare our mapDispatchToProps
with our actions:
// App.js
...
const mapDispatchToProps = dispatch => {
return {
addNote: note => dispatch(addNote(note)),
deleteNote: note => dispatch(deleteNote(note))
}
}
...
// actions/index.js
...
export const addNote = notes => {
id++;
return {
type: ADD_NOTE,
notes: {...notes, id: id}
}
}
export const deleteNote = id => ({
type: DELETE_NOTE,
id
})
They are one and the same! When we send our actions to reducer, it is said that we are "dispatching" them. We are making our redux addNote
and deleteNote
actions available to our app as this.props.addNote
and this.props.deleteNote
through mapDispatchToProps
.
Here you can see both deleteNote
and addNote
being used:
handleSubmit(e) {
const {addNote} = this.props;
const {title} = this.state;
e.preventDefault();
addNote({title}) // dispatches addNote action
this.setState({title: ''})
}
handleDelete(id) {
const {deleteNote} = this.props;
deleteNote(id); // dispatches deleteNote action
}
This is how our app executes redux action.
Testing your knowledge
Here's a challenge: try adding new action to update the notes (try not to use google immediately! Spend about 30-60 minutes struggling. That's how you'll get better)
Or another challenge: try adding completed: true/false status to indicate whether a note has ben completed. If true, change the color to light gray.
Conclusion
There you have it folks. React/ Redux. Although this is only the beginning, I hope you now understand better why we use Redux, what Redux does, and how Redux works with React.
Once you master Redux basics, I'd suggest looking up Redux middleware, especially redux-saga to handle async data.
Thanks for reading. Appreciate you spending your time reading this article.
If you have any questions, feel free to ask!