DEV Community

loading...

Connecting React with Redux

Igor Irianto
Bad puns, web development, creating stuff.
Originally published at irian.to Updated on ・7 min read

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
Enter fullscreen mode Exit fullscreen mode

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'
}
Enter fullscreen mode Exit fullscreen mode

or very simple one like this:

{
  type: TOGGLE_IS_HIDDEN,
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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:

  1. Prevent typos
  2. Keep track of all available types
  3. 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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']
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Let's go through it top to bottom.

The first line is self-explanatory:

import {ADD_NOTE, DELETE_NOTE} from "../actions/types";
Enter fullscreen mode Exit fullscreen mode

It imports the constants from types.

const initialState = [
    {title: "First Note", id: 0}
  ]
Enter fullscreen mode Exit fullscreen mode

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;
  }
}

Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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')
)


Enter fullscreen mode Exit fullscreen mode

At minimum you need:

  • a reducer function (in this case, rootReducer)
  • createStore from redux and Provider from react-redux, instantiated using createStore()
  • Wrap our app with Provider 👆 and store.

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;
Enter fullscreen mode Exit fullscreen mode

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))
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  ...
Enter fullscreen mode Exit fullscreen mode

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))
  }
}
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
  }

Enter fullscreen mode Exit fullscreen mode

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!

Discussion (1)

Collapse
mohammadmanzoor8972 profile image
Mohammad Manzoor Alam

Really nice article on redux