No description, website, or topics provided.
Switch branches/tags
Nothing to show
Clone or download
Latest commit b00fa79 Feb 7, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
README.md Update README.md Feb 7, 2019

README.md

GA-Logo

React Todo

Connecting to our express API

Today we are going to create a react application that is served on localhost:3000 and an express server that will be hosted on localhost:9000. The express API is located here.

User Stories

  • a user should be able to perform all crud operations via API calls, the given API.
  • a user should be able to click an edit button that creates a modal that is prefilled out with the data to be updated

Setting up API

  • Start the movies-json-api that you should of cloned by using nodemon

Some things to note

  1. We needed to add the .json() method of body parser in order to be able to read the body of our fetch calls from our react application, that will contain our information, as you may have guess what we are sending from the client (react) will be json. Which is the standard way of exchanging information.
app.use(bodyParser.json());
  1. We enabled cors, so we can allow our react application to make api calls to our server that is running on localhost:9000, which is our express api.
const express        = require('express');
const app            = express();
const bodyParser     = require('body-parser');
const cors           = require('cors');


require('./db/db');


// SET UP CORS AS MIDDLEWARE, SO any client can make a request to our server
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());
app.use(cors());
  1. Our responses to the client (our react app) look like the following
    res.json({
      status: 200,
      data: movies
    });

that means that any object we recieve on the client will look like the object inside of res.json so on the react side we will have something that looks like the following,

   const movies = await fetch('http://localhost:9000/api/v1/movies');
   const moviesJson = await movies.json();

Where moviesJson.data will contain the actually be the movies array that we were sending back from

On To React

Remember our goal is to create all our crud operations in our react application without refreshing the page, and we want to create a modal for our edit.

  • So lets start simple and just focus on getting all the movies from the database

  • If we think about it We'll probably need a MovieContainer to handle all our fetching and the state for the application, so lets go ahead and create that

import React, { Component } from 'react';

class MovieContainer extends Component {
  constructor(){
    super();

    this.state = {
      movies: []
    }
  }
  render(){
    return (
      <div>
        Beer Herreeeeee

      </div>
      )
  }
}

export default MovieContainer;

and go ahead and require and render it inside of App.js

import React, { Component } from 'react';
import './App.css';
import MovieContainer from './MovieContainer';


class App extends Component {
  render() {
    return (
      <div className="App">
        <MovieContainer/>
      </div>
    );
  }
}

export default App;
  • You may ask why keep the App component, maybe later on you will want to add another container, or login or something along those lines, so we are leaving the option open to easy refactoring.

  • So you'll also notice we created a state, and created a property of movies that is an array, this will hold all of our movies from our json api.

  • So what we want to do now is make a fecth call to our api and retrieve all of our movies achieving the R(ead) in CRUD. Also think about when we want to make this call? We want to make it as soon as our component is mounted to the dom so we have to call our fetch request inside of componentDidMount like the following

import React, { Component } from 'react';

class MovieContainer extends Component {
  constructor(){
    super();

    this.state = {
      movies: []
    }
  }
  componentDidMount(){
    this.getMovies()
  }
  getMovies = async () => {
    
    try {
        const response = await fetch('http://localhost:9000/api/v1/movies');

        if(response.status !== 200){
            // For http errors, Fetch doesn't reject the promise on 404 or 500
            throw Error(crimes.statusText);
          }

        const moviesParsed = await response.json();
        this.setState({movies: moviesParsed.data})
    } catch(err){
        console.log(err)
        return err
    }
  }
  render(){
    return (
      <div>
        Beer Herreeeeee
      </div>
      )
  }
}

export default MovieContainer;
  • Note We used the address of our express api in our fetch call 'http://localhost:9000/api/v1/movies',

which corresponds to this route

 router.get('/', async (req, res, next) => {

     try  {

      const allMovies = await Movie.find();

      res.json({
        status: 200,
        data: allMovies
      })

    } catch (err){

      res.send(err)

    }
});
  • Next lets go ahead and render that data
  • Now remember we just delt in our 'container component', we retrieved the data and stored the data in state
  • Now we want to present the data in a presentational component.
  • So create a Movies folder with an index.js and do the following, and on YOUR OWN pass down the movies as props from the MovieContainer. So you index.js should look something like the following
import React from 'react';


const Movies = (props) => {
  const movieList = movies.map((movie, i ) => {
    return (
      <li key={movie._id}>
        <span>{movie.title}</span><br/>
        <small>{movie.description}</small><br/>
        <button>Delete</button>
        <button>Edit</button>
    </li>)
  })

  return (
    <ul>
      {movieList}
    </ul>
    )

};

export default Movies;
  • Note we are using our database id as the key (<li key={movie._id}>) in react, which is the proper way to keep track of our elements and they are all unique, which is something the virtual dom relies on to calculate its diffing algorithmn in order to figure out what elements need to be updated in a sense.

Lets Create A Movie Now

  • So lets think about what we are trying to do. We are trying to create a new movie object with title and description properties. We want to send that object over to our api, and on a successful call we want to add that object to our array.

  • So with that in mind, Where should this happen? Lets think when we NEED to send our object over and get the response from the api, we then want to setState and update our movies array with our new object, so it makes sense to keep that function where state is!!!! In the MainContainer

  • And the other thing we need is a component that will render the CreateMovie form, that contains the information of the data we our sending to our api, so the form will look like the following,

import React, { Component } from 'react';


class CreateMovie extends Component {
  constructor(){
    super();

    this.state = {
      title: '',
      description: ''
    }
  }
  updateMovie = (e) => {

    this.setState({[e.currentTarget.name]: e.currentTarget.value});

  }

  render(){
    console.log(this.props, ' this is props')
  return (
    <form onSubmit={this.props.addMovie.bind(null, this.state)}>
      <label>
        Movie:
        <input type="text" name="title" onChange={this.updateMovie}/>
      </label>
      <label>
        Description:
        <input type="text" name="description" onChange={this.updateMovie}/>
      </label>
      <input type='Submit'/>
    </form>

    )
  }
}

export default CreateMovie;
  • Make sure you require this and render it from the MainContainer, you'll notice on the onSubmit of the form we are "lifting our state" in this component up to our MainContainer where the addMovie function that we will create exists.

  • so Now our MainContainer looks like the following

import React, { Component } from 'react';
import Movies from '../Movies';
import CreateMovie from '../CreateMovie';


class MovieContainer extends Component {
  constructor(){
    super();

    this.state = {
      movies: []
    }
  }
  componentDidMount(){
    this.getMovies()
  }
  getMovies = async () => {
    
    try {
        const response = await fetch('http://localhost:9000/api/v1/movies');

        if(response.status !== 200){
            // For http errors, Fetch doesn't reject the promise on 404 or 500
            throw Error(crimes.statusText);
          }

        const moviesParsed = await response.json();
        this.setState({movies: moviesParsed.data})
    } catch(err){
        console.log(err)
        return err
    }
  }
  addMovie = async (movie, e) => {
    e.preventDefault();
    try {
        const createdMovie = await fetch('http://localhost:9000/api/v1/movies', {
          method: 'POST',
          body: JSON.stringify(movie),
          headers:{
            'Content-Type': 'application/json'
          }
        });

        const parsedResponse = await createdMovie.json();
        this.setState({movies: [...this.state.movies, parsedResponse.data]});

    } catch(err) {
      console.log(err)
    }
  }

  render(){
    return (
      <div>
        <Movies movies={this.state.movies}/>
        <CreateMovie addMovie={this.addMovie}/>
      </div>
      )
  }
}

export default MovieContainer;
  • Notice, we gave fetch an extra argument, which is an options object that contains certain properties about our request.

  • So of course our method will be a POST because we are sending information,

  • the body of our request will be be the movie we are sending JSONified so we are sending it over in the proper format to our express server, and then we are setting up a header which is a message to our server about what type of information is being sent in the body of the request. In our case 'application/json'.

  • In the setState function that cool syntax which is populating a new array, with all the old movies represented by the threedots the (spread operator, look it up), which takes the contents of that array and puts them into the movies array, we put in the parsedResponse.data which is our created movie with the database id.

  • Also note that the movie arguement that is being passed in addMovie is the first argument because the method is being called with .bind in the CreateMovie component like the following

 <form onSubmit={this.props.addMovie.bind(null, this.state)}>
  • So we just created the C(rud) part of Crud.

Lets add delete functionality

  • So lets think what are we trying to do? We are trying to remove the movie from our array in state after we have recieved confiramtion or a "response from our api" that is was removed from the database. So if we are trying to remove something from our movies in our state it makes sense to create a function that updates in, you guessed it, the MainContainer. So our code for the delete call should look like the following.
import React, { Component } from 'react';
import Movies from '../Movies';
import CreateMovie from '../CreateMovie';

class MovieContainer extends Component {
  constructor(){
    super();

    this.state = {
      movies: []
    }
  }
  componentDidMount(){
    this.getMovies()
  }
  getMovies = async () => {
    
    try {
        const response = await fetch('http://localhost:9000/api/v1/movies');

        if(response.status !== 200){
            // For http errors, Fetch doesn't reject the promise on 404 or 500
            throw Error(crimes.statusText);
          }

        const moviesParsed = await response.json();
        this.setState({movies: moviesParsed.data})
    } catch(err){
        console.log(err)
        return err
    }
  }
  addMovie = async (movie, e) => {
    e.preventDefault();
    try {
        const createdMovie = await fetch('http://localhost:9000/api/v1/movies', {
          method: 'POST',
          body: JSON.stringify(movie),
          headers:{
            'Content-Type': 'application/json'
          }
        });

        const createdMovieJson = await createdMovie.json();
        this.setState({movies: [...this.state.movies, createdMovieJson.data]});

    } catch(err) {
      console.log(err)
    }


  }
  deleteMovie = async (id, e) => {
    console.log(id, ' this is id')
    e.preventDefault();
    try {
        const deleteMovie = await fetch('http://localhost:9000/api/v1/movies/' + id, {
          method: 'DELETE'
        });
       
        const parsedResponse = await deleteMovie.json();
        this.setState({movies: this.state.movies.filter((movie, i) => movie._id !== id)});

    } catch(err) {
      console.log(err, ' error')
    }
  }
 
  }
  render(){
    return (
      <div>
        <Movies movies={this.state.movies} deleteMovie={this.deleteMovie}/>
        <CreateMovie addMovie={this.addMovie}/>
      </div>
      )
  }
}

export default MovieContainer;
  • Notice in the deleteMovie function we are adding on the id to the end of our api call, that we are recieving in the argument of the function called at the deleteButton in the Movies component.

  • We are then using the filter method, which is a regular array method to create a new array for our state when we are using setState that meet the requirements of having the movie._id in our states movies array which represents the database id, with the id that we passed up from our deleteButton in the movies component like the following

import React from 'react';

const Movies = (props) => {
  const movieList = props.movies.map((movie, i ) => {
    console.log(movie, ' movie id')
    return (
      <li key={movie._id}>
        <span>{movie.title}</span><br/>
        <small>{movie.description}</small><br/>
        <button onClick={deleteMovie.bind(null, movie._id)}>Delete</button>
    </li>)
  })

  return (
    <ul>
      {movieList}
    </ul>
    )

};


export default Movies;
  • Just like the in the form we are using .bind in order to pass the movie._id when the onClick listener is triggered

  • So now we have finished the D(elete) in crud.

On to the UPDATE and the grand finale, the finish line, the end of time, the future.

  • So lets remember what is our goal? We want to create a modal that submits a put request to our server and when we get the response from the server that the movie has been updated in the database, we then want to find the object in our states movies array and replace it with the updatedObject from the API response.

  • now how do we handle the edit form, we want to prefill out the form with the object that is being edited. So lets think about what information do we need? Where does it exist now? And where do we want it to go? Seriously think about that alot. Well, what we have to do is get the information about what element we want to edit by clicking on an edit button that is corresponded with each individual movie id. So if we can get the id from the Movie Component we can then use that to retrieve the movie we want to edit from our movies array located in, YES, the MovieContainer, Look it is handling all the functionality for our application. Isn't that cool? Yes it is! So now knowing that, we can get the properties that we want to add to the value inputs of our form inside of our EditMovie component. So our EditMovie component will look like the following

import React, { Component } from 'react';


const EditMovie = (props) =>  {

  return (
    <div>
      <h4> Edit Movie</h4>
      <form onSubmit={props.closeAndEdit}>
        <label>
          Edit Movie:
          <input type="text" name="title" onChange={props.handleFormChange} value={props.movieToEdit.title}/>
        </label>
        <label>
          Edit Description:
          <input type="text" name="description" onChange={props.handleFormChange} value={props.movieToEdit.description}/>
        </label>
        <input type='Submit'/>
      </form>
    </div>

    )
  }

export default EditMovie;
  • Note here we are creating a presentational component. You might wonder why don't we have any state in this form? We have before. Well we will get to the problem of that a little later but let's just think about where we want the data to exist that will replace the correct movie in our MainContainer movies state array. So it makes sense to keep track of the object we want to create ,in order to update the object in our database, and ultimaley inside our states movies array. Right we are concentrating on a "Single Source of Truth" for our data. So in this way we can pass down everything our form needs from our MovieContainer, which will look like the following
import React, { Component } from 'react';
import Movies from '../Movies';
import CreateMovie from '../CreateMovie';
import EditMovie from '../EditMovie';


class MovieContainer extends Component {
  constructor(){
    super();

    this.state = {
      movies: [],
      showEdit: false,
      editMovieId: null,
      movieToEdit: {
        title: '',
        description: ''
      }
    }
  }
  componentDidMount(){
    this.getMovies()
  }
  getMovies = async () => {
    
    try {
        const response = await fetch('http://localhost:9000/api/v1/movies');

        if(response.status !== 200){
            // For http errors, Fetch doesn't reject the promise on 404 or 500
            throw Error(crimes.statusText);
          }

        const moviesParsed = await response.json();
        this.setState({movies: moviesParsed.data})
    } catch(err){
        console.log(err)
        return err
    }
  }
  addMovie = async (movie, e) => {
    e.preventDefault();
    try {
        const createdMovie = await fetch('http://localhost:9000/api/v1/movies', {
          method: 'POST',
          body: JSON.stringify(movie),
          headers:{
            'Content-Type': 'application/json'
          }
        });

        const createdMovieJson = await createdMovie.json();
        this.setState({movies: [...this.state.movies, createdMovieJson.data]});

    } catch(err) {
      console.log(err)
    }


  }
  deleteMovie = async (id, e) => {
    console.log(id, ' this is id')
    e.preventDefault();
    try {
        const deleteMovie = await fetch('http://localhost:9000/api/v1/movies/' + id, {
          method: 'DELETE'
        });
        console.log('inside try')
        const deleteMovieJson = await deleteMovie.json();
        this.setState({movies: this.state.movies.filter((movie, i) => movie._id !== id)});

    } catch(err) {
      console.log(err, ' error')
    }

  }
  closeAndEdit = async (e) => {
  }
  handleFormChange = (e) => {

    this.setState({
      movieToEdit: {
        ...this.state.movieToEdit,
        [e.target.name]: e.target.value
      }
    })
  }
  render(){
    console.log(this.state)
    return (
      <div>
        <Movies movies={this.state.movies} deleteMovie={this.deleteMovie}/>
        <CreateMovie addMovie={this.addMovie}/>
        <EditMovie closeAndEdit={this.closeAndEdit} handleFormChange={this.handleFormChange} movieToEdit={this.state.movieToEdit}/>
      </div>
      )
  }
}

export default MovieContainer;
  • Look at the props you are sending to the EditMovie Component.

  • the handleFormChange function is updating or movieToEdit object, and we are using the spread operator again, look it up! Play with it! It's powerful, but basically what it is doing is spreading out or filling all the properties from ...this.state.movieToEdit into the object it is encapsulated in, then we have our normal computed property which will match one of the properties just filled out in the object by the spread operator, so now we write over whatever value happens to be represented by e.target.name which should either be? remember???? title or description.

  • Also we passed down a function called closeAndEdit that will eventually close and make the put request.

But What information do we need to actually update our state

movieToEdit: {
        title: '',
        description: ''
      }

and where does it exist? Well it exists in the Movies component where we can create a button that calls a method that opens our modal, and gets the information about the movie we want to update. We can do it like the following

import React from 'react';


const Movies = (props) => {
  const movieList = props.movies.map((movie, i ) => {
    console.log(movie, ' movie id')
    return (
      <li key={movie._id}>
        <span>{movie.title}</span><br/>
        <small>{movie.description}</small><br/>
        <button onClick={props.deleteMovie.bind(null, movie._id)}>Delete</button>
        <button onClick={props.showModal.bind(null, movie._id)}>Edit</button>
    </li>)
  })

  return (
    <ul>
      {movieList}
    </ul>
    )

};


export default Movies;

and in our MovieContainer the showModal function will look like the following, notice we our lifting up the movie's movie._id which will allow us to find the object we want to update in our states movies array. so our state in our MainContainer looks like

 this.state = {
      movies: [],
      showEdit: false,
      editMovieId: null,
      movieToEdit: {
        title: '',
        description: ''
      }
    }

and the showModal method

  showModal = (id, e) => {
    //bindind arguments come before the event object (e), when called with bind
    const movieToEdit = this.state.movies.find((movie) => movie._id === id)
    console.log(movieToEdit, ' movieToEdit')
    this.setState({
                  showEdit: true,
                  editMovieId: id,
                  movieToEdit: movieToEdit
                  });
  }
  • Here we are using the .find method that is a javacsript array method. Go look it up, play with it on mdn, figure out how it works. But Basically what is happening we are passing a function to the find method that will return an item the meets a certain condition which is the movie._id of the object in the this.state.movies array which will match the argument id that is being passed from our editButton in the Movies Component. So now we can update our state with the correct movieToEdit which was retrieved by using the .find method on our this.state.movies array.

  • We also added a property to show or hide the EditMovie component. Which we will use later, notice it is also in state. The property is called showEdit.

  • Now we have almost everything working all we have to do is make the api call and then update the movies array. We can do that like the following

  closeAndEdit = async (e) => {
    e.preventDefault();

    try {
      const editResponse = await fetch('http://localhost:9000/api/v1/movies/' + this.state.editMovieId, {
        method: 'PUT',
        body: JSON.stringify(this.state.movieToEdit),
        headers:{
          'Content-Type': 'application/json'
        }
      });

      const parsedResponse = await editResponse.json();

      const editedMovieArray = this.state.movies.map((movie) => {

              if(movie._id === this.state.editMovieId){

                movie.title = parsedResponse.data.title;
                movie.description = parsedResponse.data.description;
              }

              return movie
      });

       this.setState({
        movie: editedMovieArray,
        showEdit: false
       });



    } catch(err) {
      console.log(err);
    }

  }
  • Here we are using the id we saved in state to add to the end of our api call that is hitting the following endpoint in our express API
router.put('/:id', async (req, res) => {

  try {
    const updatedMovie = await Movie.findByIdAndUpdate(req.params.id, req.body, {new: true});
    res.json({
      status: 200,
      data: updatedMovie
    });
  } catch(err){
    res.send(err)
  }
});
  • Notice in the body of the request
body: JSON.stringify(this.state.movieToEdit)
  • we are stringifying our object, which is neatly located in our state!!!! Aren't container components cool. Or dare I say a Smart Component...

  • after are response is parsed

const parsedResponse = await editResponse.json();

we will then created an new array that includes that editedItem returned from the API call located in the parsedResponse variable. And which is the following from above

 const parsedResponse = await editResponse.json();

  const editedMovieArray = this.state.movies.map((movie) => {

      if(movie._id === this.state.editMovieId){

        movie.title = parsedResponse.data.title;
        movie.description = parsedResponse.data.description;
      }

      return movie
});

We create the new array by using the map function, and checking to see if the movie id in the this.state.movies matches the id of the movie we edited from the object that we sent as a request. We then updated that movie with the data from our api call, and we return movie at the end so the function being operator on by .map is actually returning the object we want to fill up the arraywith that we will use to then update state like the following

const editedMovieArray = this.state.movies.map((movie) => {

          if(movie._id === this.state.editMovieId){

            movie.title = editResponseJson.data.title;
            movie.description = editResponseJson.data.description;
          }

          return movie
  });

   this.setState({
    movie: editedMovieArray,
    showEdit: false
   });

and then the final MovieContainer code, which toggles the EditMovie component

import React, { Component } from 'react';
import Movies from '../Movies';
import CreateMovie from '../CreateMovie';
import EditMovie from '../EditMovie';


class MovieContainer extends Component {
  constructor(){
    super();

    this.state = {
      movies: [],
      showEdit: false,
      editMovieId: null,
      movieToEdit: {
        title: '',
        description: ''
      }
    }
  }
  componentDidMount(){
    this.getMovies()
  }
  getMovies = async () => {
    
    try {
        const response = await fetch('http://localhost:9000/api/v1/movies');

        if(response.status !== 200){
            // For http errors, Fetch doesn't reject the promise on 404 or 500
            throw Error(crimes.statusText);
          }

        const moviesParsed = await response.json();
        this.setState({movies: moviesParsed.data})
    } catch(err){
        console.log(err)
        return err
    }
  }
  addMovie = async (movie, e) => {
    e.preventDefault();
    try {
        const createdMovie = await fetch('http://localhost:9000/api/v1/movies', {
          method: 'POST',
          body: JSON.stringify(movie),
          headers:{
            'Content-Type': 'application/json'
          }
        });

        const createdMovieJson = await createdMovie.json();
        this.setState({movies: [...this.state.movies, createdMovieJson.data]});

    } catch(err) {
      console.log(err)
    }


  }
  deleteMovie = async (id, e) => {
    console.log(id, ' this is id')
    e.preventDefault();
    try {
        const deleteMovie = await fetch('http://localhost:9000/api/v1/movies/' + id, {
          method: 'DELETE'
        });
        console.log('inside try')
        const deleteMovieJson = await deleteMovie.json();
        this.setState({movies: this.state.movies.filter((movie, i) => movie._id !== id)});

    } catch(err) {
      console.log(err, ' error')
    }








  }
  showModal = (id, e) => {
    // i comes before e, when called with bind
    const movieToEdit = this.state.movies.find((movie) => movie._id === id)
    console.log(movieToEdit, ' movieToEdit')
    this.setState({
                  showEdit: true,
                  editMovieId: id,
                  movieToEdit: movieToEdit
                  });
  }
  closeAndEdit = async (e) => {
    e.preventDefault();

    try {
      const editResponse = await fetch('http://localhost:9000/api/v1/movies/' + this.state.editMovieId, {
        method: 'PUT',
        body: JSON.stringify(this.state.movieToEdit),
        headers:{
          'Content-Type': 'application/json'
        }
      });

      const editResponseJson = await editResponse.json();

      const editedMovieArray = this.state.movies.map((movie) => {

              if(movie._id === this.state.editMovieId){

                movie.title = editResponseJson.data.title;
                movie.description = editResponseJson.data.description;
              }

              return movie
      });

       this.setState({
        movie: editedMovieArray,
        showEdit: false
       });



    } catch(err) {
      console.log(err);
    }

  }
  handleFormChange = (e) => {

    this.setState({
      movieToEdit: {
        ...this.state.movieToEdit,
        [e.target.name]: e.target.value
      }
    })
  }
  render(){
    console.log(this.state)
    return (
      <div>
        <Movies movies={this.state.movies} deleteMovie={this.deleteMovie} showModal={this.showModal}/>
        <CreateMovie addMovie={this.addMovie}/>
        {this.state.showEdit ? <EditMovie closeAndEdit={this.closeAndEdit} handleFormChange={this.handleFormChange} movieToEdit={this.state.movieToEdit}/> : null}

      </div>
      )
  }
}

export default MovieContainer;

And Thats it!

  • Observe what you built,
  • Think about the what is in the MovieContainer what is everythings purpose, why is it there?
  • Same for all your other componenets, how do they relate to the MovieContainer
  • Your EditMovie and CreateMovie components look very similar. Could this be refactored to one form? ........