DEV Community

Dorthy Thielsen
Dorthy Thielsen

Posted on

Quote Maker Lab: Redux/React

I am getting so close to finishing my bootcamp at Flatiron. I have finished React and am diving into Redux. I will say that when I started learning React, the thought of where to store state was a bit overwhelming. When I started drawing out my node trees, that got a lot easier. Then came in Redux and the idea of the store where all your state is conveniently held. I was actually a little bummed because I really felt like I had a grasp on state in React and now came in this thing to make things easier. Although I will say the text has been a little confusing where it will say to not always use the store, but use the store. Still figuring things out, but that is the whole experience of coding, right?

I wanted to go over this Redux lab in the bootcamp that seemed overwhelming when I started it. It was the first large lab in the Redux section. It is called the Quote Maker Lab. Basically you have a form where you can submit quotes and the author of those quotes and then you want those quotes to show up on the page with all the information, plus a down vote, up vote, and delete button with all their functionality built in.

To start chipping away at this lab, I first mounted the main components to my App.js, those being <QuoteForm /> and <Quotes />. By adding those to the render() my page was already starting to look better as the there was some code provided in this lab to render a basic form and the <h1>s. Always remember to import the corresponding files into App.js via import QuoteForm from "./components/QuoteForm". Also quick note: adding .js to the end of your file name is optional when importing. I personally always leave it off. Then I ran the tests provided to see where to start. The first step the tests wanted me to do was to deal with the action creators, in this case they were addQuote, removeQuote, upvoteQuote, and downvoteQuote. All action creators are just functions that you are exporting. All of these functions need to return an object with a type and a payload. I wanted to show you that you can write these either multiline or as an arrow function:

export function downvoteQuote(quoteId){
    // should return an object with a type of "DOWNVOTE_QUOTE" and a quoteId
    return{
        type: "DOWNVOTE_QUOTE",
        quoteId: quoteId
    }
}

// or as an arrow function:
export const downVote = (quoteId) => ({ type: "DOWNVOTE_QUOTE", quoteId: quoteId })
Enter fullscreen mode Exit fullscreen mode

Basically all the actions pretty much looked like this but with a different type. With all these action creators down it was on to the QuoteCard Component as that was the next one listed in the test. This component already had some code, but was missing its props to display the content. This test being next seemed a bit strange especially because we haven't gotten to how props are being passed down. In the README, the example of the object that is being created only has attributes of id, content, and author, no mention of votes. However I added in props.quote.author, props.quote.content, and props.quote.votes to the card rendering from this component. I will come back to showing the votes later on as currently this code won't work.

The next test was for the QuoteForm component which will allows us to start dealing with state and those props previously mentioned. This component had some provided code for the form, but it currently had no functionality. I first wanted to tackle the state with some key/value pairs.

  state = {
    content: "",
    author: ""
  }

Enter fullscreen mode Exit fullscreen mode

Something I immediately noticed with the form provided was that there was no unique identifier between the input fields, so I added a name to each. This will allow us to handle events easier as you will soon see. Also currently the form doesn't work when you try to type in it, so a onChange event handler needed to be added to each input. Now the inputs looked like:

                      <input
                        className="form-control"
                        type="text"
                        name="author"
                        value={this.state.author}
                        onChange={this.handleOnChange}
                      /> 
Enter fullscreen mode Exit fullscreen mode

Next was to tackle the onChange event handler to handle updating the components state and allow for the input fields to work. By previously adding the name attribute to each input field, I no longer have to write out each key/value pair in this method, but can just call on the event's target's name. A quick note: the reason event.target.name has to be in brackets is because we are getting the key from an operation and we just want the value from that operation.

  handleOnChange = event => {
    this.setState({
      [event.target.name]: event.target.value
    })
  }
Enter fullscreen mode Exit fullscreen mode

Now to tackle submitting the form. First I am going to add the onSubmit={this.handleOnSubmit} to the form so that the event can be handled. As with almost any submit, we want to preventDefault() so the page doesn't automatically refresh on submit. Then we want to create a quote object from state. In this lab we are using uuid() to create our unique id's for every instance. Next is to pass the quote object to the action creators that we created earlier in this lab. When we are submitting a form, we want to create an object so the only action creator that makes sense is addQuote. We need to connect to the store in order to do this via connect(). The thing we always need to do with actions is to dispatch them via mapDispatchToProps. This way we are getting access to dispatch so we can dispatch the return value of those actions to the reducer. This way we can call dispatch in our handleOnSubmit via this.props.dispatchAddQuote(quote). Then we want to return state to default so the form clears out.

  handleOnSubmit = event => {
    // Handle Form Submit event default
    event.preventDefault()
    // Create quote object from state
    const quote = {
      id: uuid(),
      content: this.state.content,
      author: this.state.author
    }
    // Pass quote object to action creator
    this.props.dispatchAddQuote(quote)
    // Update component state to return to default state
    this.setState({
      content: "",
      author: ""
    })
  }

const mapDispatchToProps = (dispatch) => {
  return {
    dispatchAddQuote: (quote) => dispatch(addQuote(quote))
  }
}
//add arguments to connect as needed
export default connect(null, mapDispatchToProps)(QuoteForm);
Enter fullscreen mode Exit fullscreen mode

Now to look at our reducers. First is to see how the reducers are being connected to the store in index.js.

import rootReducer from './reducers/index'

let store = createStore(rootReducer)
Enter fullscreen mode Exit fullscreen mode

Let's quickly go back to the QuoteCard even though it is revisited in the last two tests. This way we can visually see if our reducers are working. We want to render the <QuoteCard /> in our Quote container. First we must gain access to our quotes via connect() and mapStateToProps. We could just write this inline in our connect(). We are taking the state from our store and returning an object that is mapped to props. We are getting a key of quotes from our store state. This key is coming from our rootReducer, more on this later. TLDR: we are taking the state from our store and mapping it to this component as props. I will also include the way to write it not inline.

export default connect(storeState => ({quotes: storeState.quotes }))(Quotes);

// or 
const mapStateToProps = (state) => {
  return {
    quotes: state.quotes
  }
}
export default connect(mapStateToProps)(Quotes);
Enter fullscreen mode Exit fullscreen mode

Our quotes are going to be in array so we are going to have to map them in our render(). {this.props.quotes.map(q => <QuoteCard quote={q} />)}

Now to go look at that reducer. What is weird is thatrootReducer has combineReducers which isn't necessary for this project. But it does link to quotes and gives us access to our quotes array, so let's take a look at that. This is one of the few things that doesn't have any code really. As with most reducers, let start by making a switch statement. First action is "ADD_QUOTE" and we want to take the previous state and add to it so this is a great use of the spread operator or you can use .concat this way we are being non-destructive. You would never want to use .push as that is destructive and not making a copy. "REMOVE_QUOTE" is our next action. We are going to want to use filter because we want to find the specific quote and delete it. This is where having that uuid() comes in handy.

The next two reducers I had no idea where to even start because they have to deal with the upvote and downvote. Votes currently aren't stored in state at all. Let's go back to QuoteForm as that is where our default state is created. We can assume that votes start at 0 when a quote is created so we can add votes: 0 to our state. Back to our reducers. Remember that from the action, we are just getting back the id of that quote. So we need to find the quote whose id matches and then increase or decrease the votes. Also remember that this Redux so we don't want to set state here or mutate state. However we only have access to the id so how do we get the whole state of the quote? First let's actually find the index. We want to return the state up to the portion that we are altering so use slice() with our found index. That will return everything up to this quote, then we want to return the correct quote and then the rest of the state. We still don't really have the quote content so still need to figure that out. Next to find the value of the correct quote. We want to create a new object. We first want to use the spread operator to maintain the state and then pass in the key/value pair we want to change. Then we will do the same for downvotes although keep in mind that we have to make sure the number of votes is positive before we subtract a vote.

export default (state = [], action) => {
  switch(action.type){
    case "ADD_QUOTE":
      return [...state, action.quote]
      // or return state.concat(action.quote)

    case "REMOVE_QUOTE":
      return state.filter(q => q.id !== action.quoteId)

    case "UPVOTE_QUOTE":
      let quoteIndex = state.findIndex(q => q.id === action.quoteId)
      let quote = {...state[quoteIndex], votes: state[quoteIndex].votes + 1}
      return [...state.slice(0, quoteIndex), quote, ...state.slice(quoteIndex + 1)]

      case 'DOWNVOTE_QUOTE':
        let index = state.findIndex(quote => quote.id === action.quoteId);
        let quoteDown = state[index];
        if (quoteDown.votes > 0) {
          return [
            ...state.slice(0, index),
            Object.assign({}, quoteDown, { votes: quoteDown.votes -= 1 }),
            ...state.slice(index + 1)
          ];
        }
        return state;

    default:
      return state;

  }
}
Enter fullscreen mode Exit fullscreen mode

Lastly default, you just want to return state. This way something is coming back incase a random action is hit for some reason.

The last thing is to get everything running in QuoteCard. So we need to build the quotes and map them better than we previously did. Down and up votes need to be separated so this is where our action creators come back in handy. Let's import them into the Quotes container so it can be dispatched to the card as props.

import React, { Component } from "react";
import { connect } from "react-redux";
import QuoteCard from "../components/QuoteCard";
import { removeQuote, upvoteQuote, downvoteQuote } from "../actions/quotes"

class Quotes extends Component {
  buildQuotes = () => {
    return this.props.quotes.map(quote => {
      return (
      <QuoteCard 
        key={quote.id}
        quote={quote}removeQuote={this.props.removeQuote}
        upvoteQuote={this.props.upvoteQuote}
        downvoteQuote={this.props.downvoteQuote}
      />
      )
    })
  }

  render() {
    return (
      <div>
        <hr />
        <div className="row justify-content-center">
          <h2>Quotes</h2>
        </div>
        <hr />
        <div className="container">
          <div className="row">
            <div className="col-md-4">
              {this.buildQuotes()}
            </div>
          </div>
        </div>
      </div>
    );
  }
}
function mapStateToProps(store) {
  return {
    quotes: store.quotes
  }
}
//add arguments to connect as needed
export default connect(mapStateToProps, { removeQuote, upvoteQuote, downvoteQuote })(Quotes);
Enter fullscreen mode Exit fullscreen mode

Now we can call these dispatch actions on the buttons in QuoteCards.

Here is the link to the repo if you want to see the entire code. I honestly started writing this when I started up this lab not knowing how long this would take. I apologize that it gets a little rushed at the end but I was running out of time for the day and wanted to be done. Also didn't proofread so please forgive any typing errors. I really struggled with this lab and am still struggling with some of the concepts, but that is what these labs are for.

Discussion (0)