Bootcamp Notes – Day 17 (Wed) – React: Week 5 – Fetch

 Fetch

JavaScript Promises

Well there’s more than one way to handle asynchronous computation with JavaScript. Once common way is by using promises, which were added to JavaScript formally in ES6 to handle asynchronous computation.

As we discussed before, communicating with a server, the results are typically not available instantaneously. There is usually going to be a delay even if it’s very brief. So promises are a way to let your application proceed without getting stuck waiting for that response.

So what is a Promise?

Well first off, like so much in JavaScript. A promise is a special kind of object. A promise object is a proxy for a value that is not available at the moment the promise is created.

By analogy, let’s say you order a pizza. You don’t get your pizza right away, but you do have a promise that the pizzeria will bake and deliver the pizza to you. Now a promise for a pizza isn’t the pizza. You can’t eat a promise! Also, a promise is not a guarantee that you will get the pizza, anything could happen between now and when that pizza gets delivered. The pizzeria might run out of dough and not be able to make the pizza at all. The pizza delivery driver might run out of gas! A promise for a pizza is better than no promise at all! You can reasonably expect that you will either get pizza or the pizzeria will let you know if the delivery has failed for some reason.

A Promise has three states:

  • Pending – You have ordered the pizza, but it has not yet arrived.
  • Fulfilled (aka Resolved) – Your pizza has been baked and delivered.
  • Rejected – The pizza delivery has failed for some reason.

To create a promise, you would do it in a similar way as when you create an object from a class.

However it is good to new what a promise looks like, in your React application you will not actually be creating any promises instead you will be using something called the Fetch APi, which we will be learning about and the Fetch Api will actually create promises and return them to you and then you will handle those promises.

CONSUMING A PROMISE

To handle promises, which is known as consuming a promise, you will need to know about the promise object methods called:

  • .then()
  • .catch()
  • .finally()   Note: We won’t need to use that one in this course.

You will use the .then() method on a promise object and you give it a callback function that takes as is argument the value that’s returned from the promise if it’s resolved successfully. Here we are calling that value response, so that’s how you handle it when the promise is resolved. What if it’s rejected? There are two ways to handle that, .then() method has an optional second argument, where you can write another callback function that handles the error that’s returned from the promises reject function.

Another way is to chain a catch method after the .then() method and it will deal with the error if you didn’t supply the second argument to the then method.

Sometimes you’ll want to have a promise generate another promise which you can do with method training by having the .then() method return a promise then you attach another then method to that and then a catch at the end. This is called a promised chain.

And you can add as many promises as you like to this chain. At the end of a promise chain, you should typically chain a catch method if any of the promises in the chain are rejected and does not have a reject callback, the .catch() method will catch that reject.

There may be times when you want to go to the catch method right away, without going through a bunch of chain methods or any other code in between, in such cases you can use the JavaScript statement throw to go immediately to the next available catch method.

You could have combinations of both ways to handle rejects in a promise chain, where some of the .then() methods have their own specific way of handling errors, but there is still a .catch() method at the end for those that don’t.

Going back to our pizza analogy, let us see how that would look with the promised syntax. Here is the codepen demo for it.


The Fetch WEB API

What is a web APi?

Web APi’s consist of code that’s not technically part of the JavaScript Language, but are built into standard browsers by default, so that you can use them in client-side JavaScript. Actually, you might not know it but you’ve already been using many WEB APi’s.

Many examples: document.getElementByID is part of the Document Web API

Before Fetch, the most common Web APi to use for making client-side HTTP server request was the XMLHTTPRequest APi. This XML request is now considered outdated.

Fetch is meant to be its modern replacement with a more powerful and flexible feature set, but you will still see it in older code.

Some third-party library Fetch alternatives such as Axios and Superagent are built using XMLHttpRequest as the base but wrapped with more powerful code.

FETCH

In Fetch you make requests to the server and get a response and return from the server. The request and response are both represented with objects. These objects can have different properties such as:

This is an example taken from some code that we will be using in a future exercise. The Fetch function, which can also be called a global method has only one required parameter which is the path to the resource that you are trying to Fetch from like this example below. The Fetch APi is entirely promised based, when you call fetch it’s return value will be a promise containing the response object.

Be default the response will contain a boolean ok property. Fetch sets this response.ok property to true or false based on the HTTP response that it gets from the server. If it gets our status code like 404 for example, you will set it to false and if it’s within the normal range of 200 to 300 you will set it to true. So we can quickly check if a fetch was successful by checking if response.ok is true.

If not we can throw an error.

There can also be cases when you don’t get a response at all because she never reached the server to even get a status code, so the promise was rejected and we can add a second callback function here for that case and we just want to throw this error to the catch block

If the Fetch was successful we can chain a done method and here we can use a built in method of the response object called json. This method json is one that we can use if we know that the body will contain data in json format. What this method will do is return another promise that contains the data converted to a JavaScript object.

And the next then in our promise chain will dispatch the add campsites action using that JavaScript object. And if there are any issues with either the json method or the dispatch of ad campsites the catch method down here will then dispatch the action campsites failed.

This example here is a way of requesting data from the server so it’s a GET request.  We can also POST data to the server and you will see that later.


Exercise: Fetch from Server

  • Learn to incorporate Fetch into your React app.
  • Use Fetch to communicate with json-server to request resources.

Start json-server

  • Open your nucampsites project folder in VS Code.
  • In a bash terminal, navigate to the json-server folder that you set up in the previous exercise. Remember, the json-server is in the NucampFolder.
  • Double check that your bash terminal is in the json-server folder, then start json-server. Tip: Use the Up arrow key to access previous commands. If that doesn’t work for you,  here is that command to start json-server again:   json-server -H 0.0.0.0 –watch db.json -p 3001 -d 2000

Create a configuration file named baseUrl.js in the shared folder

export const baseUrl = 'http://localhost:3001/';

ActionTypes.js

export const ADD_COMMENT = ‘ADD_COMMENT’;
export const CAMPSITES_LOADING = ‘CAMPSITES_LOADING’;
export const CAMPSITES_FAILED = ‘CAMPSITES_FAILED’;
export const ADD_CAMPSITES = ‘ADD_CAMPSITES’;
export const ADD_COMMENTS = ‘ADD_COMMENTS’;
export const COMMENTS_FAILED = ‘COMMENTS_FAILED’;
export const PROMOTIONS_LOADING = ‘PROMOTIONS_LOADING’;
export const ADD_PROMOTIONS = ‘ADD_PROMOTIONS’;
export const PROMOTIONS_FAILED = ‘PROMOTIONS_FAILED’;

ActionCreators.js

import * as ActionTypes from ‘./ActionTypes’;
import { baseUrl } from ‘../shared/baseUrl’;
export const addComment = (campsiteId, rating, author, text) => ({
    type: ActionTypes.ADD_COMMENT,
    payload: {
        campsiteId: campsiteId,
        rating: rating,
        author: author,
        text: text
    }
});
export const fetchCampsites = () => dispatch => {
    dispatch(campsitesLoading());
    return fetch(baseUrl + ‘campsites’)
        .then(response => response.json())
        .then(campsites => dispatch(addCampsites(campsites)));
};
export const campsitesLoading = () => ({
    type: ActionTypes.CAMPSITES_LOADING
});
export const campsitesFailed = errMess => ({
    type: ActionTypes.CAMPSITES_FAILED,
    payload: errMess
});
export const addCampsites = campsites => ({
    type: ActionTypes.ADD_CAMPSITES,
    payload: campsites
});
export const fetchComments = () => dispatch => {    
    return fetch(baseUrl + ‘comments’)
        .then(response => response.json())
        .then(comments => dispatch(addComments(comments)));
};
export const commentsFailed = errMess => ({
    type: ActionTypes.COMMENTS_FAILED,
    payload: errMess
});
export const addComments = comments => ({
    type: ActionTypes.ADD_COMMENTS,
    payload: comments
});
export const fetchPromotions = () => dispatch => {
    dispatch(promotionsLoading());
    return fetch(baseUrl + ‘promotions’)
        .then(response => response.json())
        .then(promotions => dispatch(addPromotions(promotions)));
};
export const promotionsLoading = () => ({
    type: ActionTypes.PROMOTIONS_LOADING
});
export const promotionsFailed = errMess => ({
    type: ActionTypes.PROMOTIONS_FAILED,
    payload: errMess
});
export const addPromotions = promotions => ({
    type: ActionTypes.ADD_PROMOTIONS,
    payload: promotions
});

comments.js

import * as ActionTypes from './ActionTypes';

export const Comments = (state = { errMess: null, comments: []}, action) => {
    switch (action.type) {
        case ActionTypes.ADD_COMMENTS:
            return {...state, errMess: null, comments: action.payload};

        case ActionTypes.COMMENTS_FAILED:
            return {...state, errMess: action.payload};

        case ActionTypes.ADD_COMMENT:
            const comment = action.payload;
            comment.id = state.comments.length;
            comment.date = new Date().toISOString();
            return {...state, comments: state.comments.concat(comment)};

        default:
            return state;
    }
};

promotions.js

import * as ActionTypes from './ActionTypes';

export const Promotions = (state = { isLoading: true,
                                        errMess: null,
                                        promotions: [] }, action) => {
    switch (action.type) {
        case ActionTypes.ADD_PROMOTIONS:
            return {...state, isLoading: false, errMess: null, promotions: action.payload};

        case ActionTypes.PROMOTIONS_LOADING:
            return {...state, isLoading: true, errMess: null, promotions: []}

        case ActionTypes.PROMOTIONS_FAILED:
            return {...state, isLoading: false, errMess: action.payload};
            
        default:
            return state;
      }
};

MainComponent.j

import React, { Component } from ‘react’;
import Directory from ‘./DirectoryComponent’;
import CampsiteInfo from ‘./CampsiteInfoComponent’;
import Header from ‘./HeaderComponent’;
import Footer from ‘./FooterComponent’;
import Home from ‘./HomeComponent’;
import Contact from ‘./ContactComponent’;
import About from ‘./AboutComponent’;
import { Switch, Route, Redirect, withRouter } from ‘react-router-dom’;
import { connect } from ‘react-redux’;
import { actions } from ‘react-redux-form’;
import { addComment, fetchCampsites, fetchComments, fetchPromotions } from ‘../redux/ActionCreators’;
const mapStateToProps = state => {
    return {
        campsites: state.campsites,
        comments: state.comments,
        partners: state.partners,
        promotions: state.promotions
    };
};
const mapDispatchToProps = {
    addComment: (campsiteId, rating, author, text) => (addComment(campsiteId, rating, author, text)),
    fetchCampsites: () => (fetchCampsites()),
    resetFeedbackForm: () => (actions.reset(‘feedbackForm’)),
    fetchComments: () => (fetchComments()),
    fetchPromotions: () => (fetchPromotions())
};
class Main extends Component {
    componentDidMount() {
        this.props.fetchCampsites();
        this.props.fetchComments();
        this.props.fetchPromotions();
    }
    
    render() {
        const HomePage = () => {
            return (
                <Home
                    campsite={this.props.campsites.campsites.filter(campsite => campsite.featured)[0]}
                    campsitesLoading={this.props.campsites.isLoading}
                    campsitesErrMess={this.props.campsites.errMess}
                    promotion={this.props.promotions.promotions.filter(promotion => promotion.featured)[0]}
                    promotionLoading={this.props.promotions.isLoading}
                    promotionErrMess={this.props.promotions.errMess}
                    partner={this.props.partners.filter(partner => partner.featured)[0]}
                />
            );
        };
        const CampsiteWithId = ({match}) => {
            return (
                <CampsiteInfo
                    campsite={this.props.campsites.campsites.filter(campsite => campsite.id === +match.params.campsiteId)[0]}
                    isLoading={this.props.campsites.isLoading}
                    errMess={this.props.campsites.errMess}
                    comments={this.props.comments.comments.filter(comment => comment.campsiteId === +match.params.campsiteId)}
                    commentsErrMess={this.props.comments.errMess}
                    addComment={this.props.addComment}
                />   
            );
        };    
        return (
            <div>
                <Header />
                <Switch>
                    <Route path=’/home’ component={HomePage} />
                    <Route exact path=’/directory’ render={() => <Directory campsites={this.props.campsites} />} />
                    <Route path=’/directory/:campsiteId’ component={CampsiteWithId} />
                    <Route exact path=’/contactus’ render={() => <Contact resetFeedbackForm={this.props.resetFeedbackForm} />} />
                    <Route path=’/aboutus’ render={() => <About partners={this.props.partners} />} />
                    <Redirect to=’/home’ />
                </Switch>
                <Footer />
            </div>
        );
    }
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Main));

DirectoryComponent.js

import React from ‘react’;
import { Card, CardImg, CardImgOverlay, CardTitle, Breadcrumb, BreadcrumbItem } from ‘reactstrap’;
import { Link } from ‘react-router-dom’;
import { Loading } from ‘./LoadingComponent’;
import { baseUrl } from ‘../shared/baseUrl’;
function RenderDirectoryItem({campsite}) {
    return (
        <Card>
            <Link to={`/directory/${campsite.id}`}>
                <CardImg width=”100%” src={baseUrl + campsite.image} alt={campsite.name} />
                <CardImgOverlay>
                    <CardTitle>{campsite.name}</CardTitle>
                </CardImgOverlay>
            </Link>
        </Card>
    );
}
function Directory(props) {
    const directory = props.campsites.campsites.map(campsite => {
        return (
            <div key={campsite.id} className=”col-md-5 m-1″>
                <RenderDirectoryItem campsite={campsite} />
            </div>
        );
    });
    if (props.campsites.isLoading) {
        return (
            <div className=”container”>
                <div className=”row”>
                    <Loading />
                </div>
            </div>
        );
    }
    if (props.campsites.errMess) {
        return (
            <div className=”container”>
                <div className=”row”>
                    <div className=”col”>
                        <h4>{props.campsites.errMess}</h4>
                    </div>
                </div>
            </div>
        );
    }
    return (
        <div className=”container”>
        <div className=”row”>
            <div className=”col”>
                <Breadcrumb>
                    <BreadcrumbItem><Link to=”/home”>Home</Link></BreadcrumbItem>
                    <BreadcrumbItem active>Directory</BreadcrumbItem>
                </Breadcrumb>
                <h2>Directory</h2>
                <hr />
            </div>
        </div>
        <div className=”row”>
            {directory}
        </div>
    </div>
    );
}
export default Directory;

HomeComponent.j

import React from ‘react’;
import { Card, CardImg, CardText, CardBody, CardTitle } from ‘reactstrap’;
import { Loading } from ‘./LoadingComponent’;
import { baseUrl } from ‘../shared/baseUrl’;
function RenderCard({item, isLoading, errMess}) {
    if (isLoading) {
        return <Loading />;
    }
    if (errMess) {
        return <h4>{errMess}</h4>;
    }
    return (
        <Card>
            <CardImg src={baseUrl + item.image} alt={item.name} />
            <CardBody>
                <CardTitle>{item.name}</CardTitle>
                <CardText>{item.description}</CardText>
            </CardBody>
        </Card>
    );
}
function Home(props) {
    return (
        <div className=”container”>
            <div className=”row”>
                <div className=”col-md m-1″>
                    <RenderCard
                        item={props.campsite}
                        isLoading={props.campsitesLoading}
                        errMess={props.campsitesErrMess}
                    />
                </div>
                <div className=”col-md m-1″>
                    <RenderCard
                        item={props.promotion}
                        isLoading={props.promotionLoading}
                        errMess={props.promotionErrMess}
                    />
                </div>
                <div className=”col-md m-1″>
                    <RenderCard item={props.partner} />
                </div>
            </div>
        </div>
    );
}
export default Home;

CampsiteInfoComponent.js

import React from ‘react’;
import { Card, CardImg, CardText, CardBody, Breadcrumb, BreadcrumbItem, Button, Modal, ModalHeader, ModalBody, Col, Row, Label } from ‘reactstrap’;
import { Link } from ‘react-router-dom’;
import { Control, LocalForm, Errors } from ‘react-redux-form’;
import { Loading } from ‘./LoadingComponent’;
import { baseUrl } from ‘../shared/baseUrl’;
const required = val => val && val.length;
const maxLength = len => val => !val || (val.length <= len);
const minLength = len => val => val && (val.length >= len);
const isNumber = val => !isNaN(+val);
const validEmail = val => /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(val);
class CommetForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isModalOpen: false
    };
    
    this.toggleModal = this.toggleModal.bind(this);
  
}
handleSubmit(values) {
  this.toggleModal();
  this.props.addComment(this.props.campsiteId, values.rating, values.author, values.text);
}
toggleModal() {
    this.setState({
        isModalOpen: !this.state.isModalOpen
    });
}
  render() {
    return (
      <div>
        <Button onClick={this.toggleModal} outline ><i className=”fa fa-pencil fa-lg” />Submit Comment</Button>{‘ ‘}
        <Modal isOpen={this.state.isModalOpen} toggle={this.toggleModal}>
                    <ModalHeader toggle={this.toggleModal}>Submit Comment</ModalHeader>
                    <ModalBody>
                    <LocalForm onSubmit={values => this.handleSubmit(values)}>
                          <div className=”form-group”>
                                <Label htmlFor=”rating”>Rating</Label>
                                <Control.select model=”.rating” id=”rating” name=”rating”
                                    className=”form-control”>
                                    <option>1</option>
                                    <option>2</option>
                                    <option>3</option>
                                    <option>4</option>
                                    <option>5</option>
                                </Control.select>
                            </div>
                            <div className=”form-group”>
                                <Label htmlFor=”author” >Your Name</Label>
                              
                                    <Control.text model=”.author” id=”author” name=”author”
                                        placeholder=”Your Name”
                                        className=”form-control”
                                        validators={{
                                            required, 
                                            minLength: minLength(2),
                                            maxLength: maxLength(15)
                                        }}
                                    />
                                    <Errors
                                        className=”text-danger”
                                        model=”.firstName”
                                        show=”touched”
                                        component=”div”
                                        messages={{
                                            required: ‘Required’,
                                            minLength: ‘Must be at least 2 characters’,
                                            maxLength: ‘Must be 15 characters or less’
                                        }}
                                    />
                            
                            </div>
                            
                           
                            <div className=”form-group”>
                                <Label htmlFor=”text” >Comment</Label>
                              
                                    <Control.textarea model=”.text” id=”text” name=”text”
                                        rows=”12″
                                        className=”form-control”
                                    />
                              
                            </div>
                         
                                    <Button type=”submit” color=”primary”>
                                        Submit
                                    </Button>
                         
                        </LocalForm>
                    </ModalBody>
                </Modal>
      </div>
    );
  }
}
function RenderCampsite({campsite}) {
    return (
      <div className=”col-md-5 m-1″>
        <Card>
          <CardImg top src={baseUrl + campsite.image} alt={campsite.name} />
          <CardBody>
            <CardText>{campsite.description}</CardText>
          </CardBody>
        </Card>
      </div>
    );
  }
  function RenderComments({comments, addComment, campsiteId}) {
    if (comments) {
      return (
        <div className=”col-md-5 m-1″>
          <h4>Comments</h4>
          {comments.map((comment) => {
            return (
              <p key={comment.id}>
                {comment.text} <br />
                {comment.author}{” “}
                {new Intl.DateTimeFormat(“en-US”, {
                  year: “numeric”,
                  month: “short”,
                  day: “2-digit”,
                }).format(new Date(Date.parse(comment.date)))}
              </p>
            );
          })}
          <CommetForm campsiteId={campsiteId} addComment={addComment} />
        </div>
      );
    }
    return <div />;
  }
  function CampsiteInfo(props) {
    if (props.isLoading) {
      return (
          <div className=”container”>
              <div className=”row”>
                  <Loading />
              </div>
          </div>
      );
  }
  if (props.errMess) {
      return (
          <div className=”container”>
              <div className=”row”>
                  <div className=”col”>
                      <h4>{props.errMess}</h4>
                  </div>
              </div>
          </div>
      );
  }
    if (props.campsite) {
        return (
          <div className=”container”>
          <div className=”row”>
              <div className=”col”>
                  <Breadcrumb>
                      <BreadcrumbItem><Link to=”/directory”>Directory</Link></BreadcrumbItem>
                      <BreadcrumbItem active>{props.campsite.name}</BreadcrumbItem>
                  </Breadcrumb>
                  <h2>{props.campsite.name}</h2>
                  <hr />
              </div>
          </div>
          <div className=”row”>
              <RenderCampsite campsite={props.campsite} />
              <RenderComments 
                        comments={props.comments}
                        addComment={props.addComment}
                        campsiteId={props.campsite.id}
                    />
          </div>
      </div>
        );
    }
    return <div />;
}
export default CampsiteInfo;

 

 

 

  • Save and test all the changes.
    • When you test this code in your browser, make sure that you start json-server first. Make sure to start it from the json-server folder.
    • You will see a broken image for the featured partner image on the home page, don’t fret! This is expected behavior, and it will be part of your assignment tasks in the workshop to fix it. Leave it as it is for now.
    • If you are getting a “Failed to Fetch” error in your browser, try replacing your baseUrl ‘localhost‘ with ‘127.0.0.1‘ as you see below, and see if that helps: export const baseUrl = 'http://127.0.0.1:3001/';

Exercise: Fetch Handling Errors

  • Update your code to deal with errors encountered while communicating with the server.

ActionCreators.js

import * as ActionTypes from ‘./ActionTypes’;
import { baseUrl } from ‘../shared/baseUrl’;
export const addComment = (campsiteId, rating, author, text) => ({
    type: ActionTypes.ADD_COMMENT,
    payload: {
        campsiteId: campsiteId,
        rating: rating,
        author: author,
        text: text
    }
});
export const fetchCampsites = () => dispatch => {
    dispatch(campsitesLoading());
    return fetch(baseUrl + ‘campsites’)
        .then(response => {
                if (response.ok) {
                    return response;
                } else {
                    const error = new Error(`Error ${response.status}: ${response.statusText}`);
                    error.response = response;
                    throw error;
                }
            },
            error => {
                const errMess = new Error(error.message);
                throw errMess;
            }
        )
        .then(response => response.json())
        .then(campsites => dispatch(addCampsites(campsites)))
        .catch(error => dispatch(campsitesFailed(error.message)));
};
export const campsitesLoading = () => ({
    type: ActionTypes.CAMPSITES_LOADING
});
export const campsitesFailed = errMess => ({
    type: ActionTypes.CAMPSITES_FAILED,
    payload: errMess
});
export const addCampsites = campsites => ({
    type: ActionTypes.ADD_CAMPSITES,
    payload: campsites
});
export const fetchComments = () => dispatch => {
    return fetch(baseUrl + ‘comments’)
        .then(response => {
                if (response.ok) {
                    return response;
                } else {
                    const error = new Error(`Error ${response.status}: ${response.statusText}`);
                    error.response = response;
                    throw error;
                }
            },
            error => {
                const errMess = new Error(error.message);
                throw errMess;
            }
        )
        .then(response => response.json())
        .then(comments => dispatch(addComments(comments)))
        .catch(error => dispatch(commentsFailed(error.message)));
};
export const commentsFailed = errMess => ({
    type: ActionTypes.COMMENTS_FAILED,
    payload: errMess
});
export const addComments = comments => ({
    type: ActionTypes.ADD_COMMENTS,
    payload: comments
});
export const fetchPromotions = () => dispatch => {
    dispatch(promotionsLoading());
    return fetch(baseUrl + ‘promotions’)
        .then(response => {
                if (response.ok) {
                    return response;
                } else {
                    const error = new Error(`Error ${response.status}: ${response.statusText}`);
                    error.response = response;
                    throw error;
                }
            },
            error => {
                const errMess = new Error(error.message);
                throw errMess;
            }
        )
        .then(response => response.json())
        .then(promotions => dispatch(addPromotions(promotions)))
        .catch(error => dispatch(promotionsFailed(error.message)));
};
export const promotionsLoading = () => ({
    type: ActionTypes.PROMOTIONS_LOADING
});
export const promotionsFailed = errMess => ({
    type: ActionTypes.PROMOTIONS_FAILED,
    payload: errMess
});
export const addPromotions = promotions => ({
    type: ActionTypes.ADD_PROMOTIONS,
    payload: promotions
});

 Exercise: Fetch Post Comment

  • Learn to use Fetch to send a POST request to the server, then process the response.

ActionCreators.js

import * as ActionTypes from ‘./ActionTypes’;
import { baseUrl } from ‘../shared/baseUrl’;
export const fetchCampsites = () => dispatch => {
    dispatch(campsitesLoading());
    return fetch(baseUrl + ‘campsites’)
        .then(response => {
                if (response.ok) {
                    return response;
                } else {
                    const error = new Error(`Error ${response.status}: ${response.statusText}`);
                    error.response = response;
                    throw error;
                }
            },
            error => {
                const errMess = new Error(error.message);
                throw errMess;
            }
        )
        .then(response => response.json())
        .then(campsites => dispatch(addCampsites(campsites)))
        .catch(error => dispatch(campsitesFailed(error.message)));
};
export const campsitesLoading = () => ({
    type: ActionTypes.CAMPSITES_LOADING
});
export const campsitesFailed = errMess => ({
    type: ActionTypes.CAMPSITES_FAILED,
    payload: errMess
});
export const addCampsites = campsites => ({
    type: ActionTypes.ADD_CAMPSITES,
    payload: campsites
});
export const fetchComments = () => dispatch => {
    return fetch(baseUrl + ‘comments’)
        .then(response => {
                if (response.ok) {
                    return response;
                } else {
                    const error = new Error(`Error ${response.status}: ${response.statusText}`);
                    error.response = response;
                    throw error;
                }
            },
            error => {
                const errMess = new Error(error.message);
                throw errMess;
            }
        )
        .then(response => response.json())
        .then(comments => dispatch(addComments(comments)))
        .catch(error => dispatch(commentsFailed(error.message)));
};
export const commentsFailed = errMess => ({
    type: ActionTypes.COMMENTS_FAILED,
    payload: errMess
});
export const addComments = comments => ({
    type: ActionTypes.ADD_COMMENTS,
    payload: comments
});
export const addComment = comment => ({
    type: ActionTypes.ADD_COMMENT,
    payload: comment
});
export const postComment = (campsiteId, rating, author, text) => dispatch => {
    
    const newComment = {
        campsiteId: campsiteId,
        rating: rating,
        author: author,
        text: text
    };
    newComment.date = new Date().toISOString();
    return fetch(baseUrl + ‘comments’, {
            method: “POST”,
            body: JSON.stringify(newComment),
            headers: {
                “Content-Type”: “application/json”
            }
        })
        .then(response => {
                if (response.ok) {
                    return response;
                } else {
                    const error = new Error(`Error ${response.status}: ${response.statusText}`);
                    error.response = response;
                    throw error;
                }
            },
            error => { throw error; }
        )
        .then(response => response.json())
        .then(response => dispatch(addComment(response)))
        .catch(error => {
            console.log(‘post comment’, error.message);
            alert(‘Your comment could not be posted\nError: ‘ + error.message);
        });
};
export const fetchPromotions = () => dispatch => {
    dispatch(promotionsLoading());
    return fetch(baseUrl + ‘promotions’)
        .then(response => {
                if (response.ok) {
                    return response;
                } else {
                    const error = new Error(`Error ${response.status}: ${response.statusText}`);
                    error.response = response;
                    throw error;
                }
            },
            error => {
                const errMess = new Error(error.message);
                throw errMess;
            }
        )
        .then(response => response.json())
        .then(promotions => dispatch(addPromotions(promotions)))
        .catch(error => dispatch(promotionsFailed(error.message)));
};
export const promotionsLoading = () => ({
    type: ActionTypes.PROMOTIONS_LOADING
});
export const promotionsFailed = errMess => ({
    type: ActionTypes.PROMOTIONS_FAILED,
    payload: errMess
});
export const addPromotions = promotions => ({
    type: ActionTypes.ADD_PROMOTIONS,
    payload: promotions
});

comments.js

import * as ActionTypes from ‘./ActionTypes’;
export const Comments = (state = { errMess: null, comments: []}, action) => {
    switch (action.type) {
        case ActionTypes.ADD_COMMENTS:
            return {…state, errMess: null, comments: action.payload};
        case ActionTypes.COMMENTS_FAILED:
            return {…state, errMess: action.payload};
        case ActionTypes.ADD_COMMENT:
            const comment = action.payload;
            return {…state, comments: state.comments.concat(comment)};
        default:
            return state;
    }
};

MainComponent.js

import React, { Component } from ‘react’;
import Directory from ‘./DirectoryComponent’;
import CampsiteInfo from ‘./CampsiteInfoComponent’;
import Header from ‘./HeaderComponent’;
import Footer from ‘./FooterComponent’;
import Home from ‘./HomeComponent’;
import Contact from ‘./ContactComponent’;
import About from ‘./AboutComponent’;
import { Switch, Route, Redirect, withRouter } from ‘react-router-dom’;
import { connect } from ‘react-redux’;
import { actions } from ‘react-redux-form’;
import { postComment, fetchCampsites, fetchComments, fetchPromotions } from ‘../redux/ActionCreators’;
const mapStateToProps = state => {
    return {
        campsites: state.campsites,
        comments: state.comments,
        partners: state.partners,
        promotions: state.promotions
    };
};
const mapDispatchToProps = {
    postComment: (campsiteId, rating, author, text) => (postComment(campsiteId, rating, author, text)),
    fetchCampsites: () => (fetchCampsites()),
    resetFeedbackForm: () => (actions.reset(‘feedbackForm’)),
    fetchComments: () => (fetchComments()),
    fetchPromotions: () => (fetchPromotions())
};
class Main extends Component {
    componentDidMount() {
        this.props.fetchCampsites();
        this.props.fetchComments();
        this.props.fetchPromotions();
    }
    
    render() {
        const HomePage = () => {
            return (
                <Home
                    campsite={this.props.campsites.campsites.filter(campsite => campsite.featured)[0]}
                    campsitesLoading={this.props.campsites.isLoading}
                    campsitesErrMess={this.props.campsites.errMess}
                    promotion={this.props.promotions.promotions.filter(promotion => promotion.featured)[0]}
                    promotionLoading={this.props.promotions.isLoading}
                    promotionErrMess={this.props.promotions.errMess}
                    partner={this.props.partners.filter(partner => partner.featured)[0]}
                />
            );
        };
        const CampsiteWithId = ({match}) => {
            return (
                <CampsiteInfo
                    campsite={this.props.campsites.campsites.filter(campsite => campsite.id === +match.params.campsiteId)[0]}
                    isLoading={this.props.campsites.isLoading}
                    errMess={this.props.campsites.errMess}
                    comments={this.props.comments.comments.filter(comment => comment.campsiteId === +match.params.campsiteId)}
                    commentsErrMess={this.props.comments.errMess}
                    postComment={this.props.postComment}
                />   
            );
        };    
        return (
            <div>
                <Header />
                <Switch>
                    <Route path=’/home’ component={HomePage} />
                    <Route exact path=’/directory’ render={() => <Directory campsites={this.props.campsites} />} />
                    <Route path=’/directory/:campsiteId’ component={CampsiteWithId} />
                    <Route exact path=’/contactus’ render={() => <Contact resetFeedbackForm={this.props.resetFeedbackForm} />} />
                    <Route path=’/aboutus’ render={() => <About partners={this.props.partners} />} />
                    <Redirect to=’/home’ />
                </Switch>
                <Footer />
            </div>
        );
    }
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Main));

 

CampsiteInfoComponent.js

import React from ‘react’;
import { Card, CardImg, CardText, CardBody, Breadcrumb, BreadcrumbItem, Button, Modal, ModalHeader, ModalBody, Col, Row, Label } from ‘reactstrap’;
import { Link } from ‘react-router-dom’;
import { Control, LocalForm, Errors } from ‘react-redux-form’;
import { Loading } from ‘./LoadingComponent’;
import { baseUrl } from ‘../shared/baseUrl’;
const required = val => val && val.length;
const maxLength = len => val => !val || (val.length <= len);
const minLength = len => val => val && (val.length >= len);
const isNumber = val => !isNaN(+val);
const validEmail = val => /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(val);
class CommetForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isModalOpen: false
    };
    
    this.toggleModal = this.toggleModal.bind(this);
  
}
handleSubmit(values) {
  this.toggleModal();
  this.props.postComment(this.props.campsiteId, values.rating, values.author, values.text);
}
toggleModal() {
    this.setState({
        isModalOpen: !this.state.isModalOpen
    });
}
  render() {
    return (
      <div>
        <Button onClick={this.toggleModal} outline ><i className=”fa fa-pencil fa-lg” />Submit Comment</Button>{‘ ‘}
        <Modal isOpen={this.state.isModalOpen} toggle={this.toggleModal}>
                    <ModalHeader toggle={this.toggleModal}>Submit Comment</ModalHeader>
                    <ModalBody>
                    <LocalForm onSubmit={values => this.handleSubmit(values)}>
                          <div className=”form-group”>
                                <Label htmlFor=”rating”>Rating</Label>
                                <Control.select model=”.rating” id=”rating” name=”rating”
                                    className=”form-control”>
                                    <option>1</option>
                                    <option>2</option>
                                    <option>3</option>
                                    <option>4</option>
                                    <option>5</option>
                                </Control.select>
                            </div>
                            <div className=”form-group”>
                                <Label htmlFor=”author” >Your Name</Label>
                              
                                    <Control.text model=”.author” id=”author” name=”author”
                                        placeholder=”Your Name”
                                        className=”form-control”
                                        validators={{
                                            required, 
                                            minLength: minLength(2),
                                            maxLength: maxLength(15)
                                        }}
                                    />
                                    <Errors
                                        className=”text-danger”
                                        model=”.firstName”
                                        show=”touched”
                                        component=”div”
                                        messages={{
                                            required: ‘Required’,
                                            minLength: ‘Must be at least 2 characters’,
                                            maxLength: ‘Must be 15 characters or less’
                                        }}
                                    />
                            
                            </div>
                            
                           
                            <div className=”form-group”>
                                <Label htmlFor=”text” >Comment</Label>
                              
                                    <Control.textarea model=”.text” id=”text” name=”text”
                                        rows=”12″
                                        className=”form-control”
                                    />
                              
                            </div>
                         
                                    <Button type=”submit” color=”primary”>
                                        Submit
                                    </Button>
                         
                        </LocalForm>
                    </ModalBody>
                </Modal>
      </div>
    );
  }
}
function RenderCampsite({campsite}) {
    return (
      <div className=”col-md-5 m-1″>
        <Card>
          <CardImg top src={baseUrl + campsite.image} alt={campsite.name} />
          <CardBody>
            <CardText>{campsite.description}</CardText>
          </CardBody>
        </Card>
      </div>
    );
  }
  function RenderComments({comments, postComment, campsiteId}) {
    if (comments) {
      return (
        <div className=”col-md-5 m-1″>
          <h4>Comments</h4>
          {comments.map((comment) => {
            return (
              <p key={comment.id}>
                {comment.text} <br />
                {comment.author}{” “}
                {new Intl.DateTimeFormat(“en-US”, {
                  year: “numeric”,
                  month: “short”,
                  day: “2-digit”,
                }).format(new Date(Date.parse(comment.date)))}
              </p>
            );
          })}
          <CommetForm campsiteId={campsiteId} postComment={postComment} />
        </div>
      );
    }
    return <div />;
  }
  function CampsiteInfo(props) {
    if (props.isLoading) {
      return (
          <div className=”container”>
              <div className=”row”>
                  <Loading />
              </div>
          </div>
      );
  }
  if (props.errMess) {
      return (
          <div className=”container”>
              <div className=”row”>
                  <div className=”col”>
                      <h4>{props.errMess}</h4>
                  </div>
              </div>
          </div>
      );
  }
    if (props.campsite) {
        return (
          <div className=”container”>
          <div className=”row”>
              <div className=”col”>
                  <Breadcrumb>
                      <BreadcrumbItem><Link to=”/directory”>Directory</Link></BreadcrumbItem>
                      <BreadcrumbItem active>{props.campsite.name}</BreadcrumbItem>
                  </Breadcrumb>
                  <h2>{props.campsite.name}</h2>
                  <hr />
              </div>
          </div>
          <div className=”row”>
              <RenderCampsite campsite={props.campsite} />
              <RenderComments 
                        comments={props.comments}
                        postComment={props.postComment}
                        campsiteId={props.campsite.id}
                    />
          </div>
      </div>
        );
    }
    return <div />;
}
export default CampsiteInfo;

Additional Resources: