Bootcamp Notes – Day 15 (Mon) – React: Week 5 – Redux Actions and Reducers

Redux Actions and Reducers

Overview for Redux Actions and Reducers

Last week, you began learning about Redux. You took a look at how it manages the state for your React Application through the use of a single store, reducers and actions.

Redux – Manages application state through the use of a single store, reducers, and actions.

But what are actions exactly?

Redux Actions – Plain JavaScript objects containing payloads of information to be sent to the Redux store.

Actions are the only way that updates get sent to the store. The only required property for an action object is type – the type will be a unique String

Best practice: Gather all action types into a separate module.

Here is an example of an actiontypes.js module in it’s final form. There are multiple benefits to this approach. Primarily you are able to easily see all the action types in one place, which is very helpful when there are multiple developers on a project. You can see in one file all the different kinds of changes to the state that have already been implemented in the app.


DISPATCHING AN ACTION

An action gets dispatched by a React component in response to some user interaction with the view. This could be the user loading the view in the browser for the first time. Clicking on a button, submitting a form etc.

React components will dispatch an action in response to some user interaction – eg. loading a new view, clicking a button, etc.

When Redux is used with React, you will never dispatch the action directly. Instead you will use the connect function to do it for you by providing it with an argument that’s typically called mapDispatchToProps . We will discuss mapDispatchToProps in more detail later.

In react-redux, you will never dispatch the action directly-you will supply an argument called mapDispatchToProps to the connect() function, which will call the store’s dispatch method for you.

Every time that an action is dispatched, a new action object must be created.

There is a special type of function in Redux called an action creator and is used to create action objects. It is not required to use action creators to create objects, but it is helpful in making sure that objects are always created in a consistent way and to prevent typographical errors.

Typically, you will write a function called an action creator that will create the object in a consistent, error-free way.

Here is an example of an action creator function that you will be using. This action creator will add an array of campsites to the Redux store. It has the required type property and it has another property called payload. We have to call the identifier for the type property – type. Payload is arbitrary we could call it something else. There is no limit to the number of properties you can have in an action. For this particular action the payload contains an array of campsites that’s received as a function parameter. Notice that this is an arrow function and we are only returning one thing, which is this object inside a parentheses, we don’t have to use the return keyword. Remember that with arrow functions, if you leave out the curly braces around the function body and there’s only one expression in the function, you don’t need to use the return keyword. It is implied. Now, you might be thinking, but wait there are curly braces. Well curly braces do a lot of different things in JavaScript and in this case these curly braces are being used to create an object. They are not the curly braces around the function body and that’s the reason that we have these parentheses around these curly braces, so that we don’t confuse the arrow function into thinking that these curly braces are for the function body, so even though you don’t see the return keyword here, be aware that this function is returning this object.

RECAP

For every action, you will create:

  1. An action type defined as String constant
  2. An action creator, a function that returns the action object and has a type property, plus any data (payload) to send to the store to update it

Then you will connect each action to a React component using the connect() function and a mapDispatchToProps argument.

Every time an action is dispatched, the Redux store will check all its reducers for a matching action type, then run the code for that action type, which will then update the state.


Reducers

Now we will discuss Reducers. Here is an example of one:

Reducers are considered to be part of the store. Now, while there are not multiple stores in Redux. There can be multiple reducers that handle different parts of the same state. By the time you reach the exercise  this simplified code is from , you will have done something called splitting the reducer, which creates different reducers to handle different parts of the state. So that the state that’s referenced in this reducer is just a section of the entire state object that this reducer is responsible for updating. Here this reducer takes it’s section of the existing state as it’s first argument.

If that state does not exist yet, then it’s using default function parameters here to initialize it to this object, which has the property campsites set to an empty array.

Then for its second parameter it takes the action object that was dispatched.

The it goes through this switch statement and if any of the action types in the switch cases match the action type  of the action object. It then creates the new state from the existing state updating it with the date from the action. Then it returns the new state to the store.

Please note, that it is not required for the reducer to use a switch statement, but it is the best way to do it. You could use if statements or other ways to check for the action type. The only thing that’s required of this reducer is that it return it’s section of the state. Again, the Redux principle is state is read only. You never mutate the existing state. You only replace it with a new state.

One of the ways you can create a new state from the existing state is by use of the spread syntax. It’s not the only way but it’s one that will often be used.

Here we are spreading out the property key value pairs from the existing state. Doing this does not mutate the existing state. We are basically making a copy of it’s properties,

then we add the payload from the action object into the campsite’s property, then we have this all surrounded in the curly braces that signify the creation of a new object. The object literal syntax. So remember from the lesson on the spread syntax, since this campsite’s property is second in order in the object literal definition, it will overwrite the campsite’s property that was spread from the existing state.

Then if none of the action types matched,  this default case will just return the existing state.

Let us go back to a concept mentioned before.

Splitting the Reducer

Last week we just had a single file reducer.js and it contained a single reducer function. That reducer is working with the entire state, which consists of four properties, campsites, comments, partners and promotions. These properties are all independent of each other and can be managed separately.

Often it will be useful to split up your reducer into multiple reducers and put them in separate files. However, Redux’s create store function will only accept a single reducer as an argument. So Redux also provides a function called combined reducers that will take all of your reducers and combine them into a single root reducer to use with create store and that’s what you will be doing in the next exercise! The after that, you will begin to implement Redux actions with action types and action creators.


 Exercise: Splitting and Combining Reducers

We will be doing in this exercise:

  • Implement separate reducers that are responsible for only part of the state.
  • Combine the reducers into a single root reducer to manage the entire state.

Create separate reducers:

campsites.js

import { CAMPSITES } from '../shared/campsites';

export const Campsites = (state = CAMPSITES, action) => {
    switch (action.type) {
        default:
            return state;
    }
};

comments.js

import { COMMENTS } from '../shared/comments';

export const Comments = (state = COMMENTS, action) => {
    switch (action.type) {
        default:
            return state;
    }
};

partners.js

import { PARTNERS } from '../shared/partners';

export const Partners = (state = PARTNERS, action) => {
    switch (action.type) {
        default:
            return state;
    }
};

promotions.js

import { PROMOTIONS } from '../shared/promotions';

export const Promotions = (state = PROMOTIONS, action) => {
    switch (action.type) {
        default:
            return state;
    }
};

 

The Redux createStore() function requires that all your reducers be combined into one single root reducer to be used as an argument to createStore(). You will use the combineReducers() function for this.

configureStore.js

import {createStore, combineReducers} from 'redux';
import { Campsites } from './campsites';
import { Comments } from './comments';
import { Partners } from './partners';
import { Promotions } from './promotions';

export const ConfigureStore = () => {
    const store = createStore(
        combineReducers({
            campsites: Campsites,
            comments: Comments,
            partners: Partners,
            promotions: Promotions
        })
    );

    return store;
};

 

Now we can safely delete the reducer.js file from the project, as it is no longer being used.


Exercise: Redux Actions

We will:

  • Define Redux actions and implement an action creator.
  • Dispatch an action from the action creator to update the state in the Redux store

ActionTypes.js

export const ADD_COMMENT = 'ADD_COMMENT'; ActionCreators.js
import * as ActionTypes from './ActionTypes';

export const addComment = (campsiteId, rating, author, text) => ({
    type: ActionTypes.ADD_COMMENT,
    payload: {
        campsiteId: campsiteId,
        rating: rating,
        author: author,
        text: text
    }
});

comments.js

import { COMMENTS } from '../shared/comments';
import * as ActionTypes from './ActionTypes';

export const Comments = (state = COMMENTS, action) => {
    switch (action.type) {
        case ActionTypes.ADD_COMMENT:
            const comment = action.payload;
            comment.id = state.length;
            comment.date = new Date().toISOString();
            return state.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 { addComment } 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))
};
class Main extends Component {
    
    render() {
        const HomePage = () => {
            return (
                <Home 
                    campsite={this.props.campsites.filter(campsite => campsite.featured)[0]}
                    promotion={this.props.promotions.filter(promotion => promotion.featured)[0]}
                    partner={this.props.partners.filter(partner => partner.featured)[0]}
                />
            );
        };
        const CampsiteWithId = ({match}) => {
            return (
                <CampsiteInfo 
                    campsite={this.props.campsites.filter(campsite => campsite.id === +match.params.campsiteId)[0]} 
                    comments={this.props.comments.filter(comment => comment.campsiteId === +match.params.campsiteId)}
                    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’ component={Contact} />
                    <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’;
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={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.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;

Redux Middleware and Redux Thunk

 

Redux Middleware

In the previous lesson we saw how we can change a state in the Redux store by dispatching actions. At times, when an action is dispatched we may want to intercept that action before it reaches the reducer and either change it or cause some side effect to happen. Something other than changing the application state.

Redux MiddlewareIntercepts an action before it reaches the reducer and allows you to change it or cause a side effect to happen, apart from updating the state – third party extension point.

For example, we might want to write a message to a log file or we might want to make a server request and wait for the server to respond. This is where Redux Middleware can help.

Redux Middleware provides the capability to run code after an action is dispatched and before it reaches the reducer. So middleware provides a point where you can inject third-party extensions that can respond to an action. One straightforward example is Redux logger.

Asynchronous Calls

Redux Middleware is also used to help you deal with asynchronous calls.

Synchronous code: Everything happens in order, app waits until a piece of code is finished before continuing

Asynchronous code: Start something, then continue with the program without waiting for it to finish, result is dealt with later

Common asynchronous call: Client requests data from server, must wait for a response before using data.

So how do we have an action request data from a server, then wait for and handle the server response? This is where Redux Middleware can help us!

The middleware can wrap around dispatch and insert operation before action reaches reducer: logging, making async calls, crash reporting, stopping an action, dispatching other actions, etc.

Middleware can be chained, so multiple middleware libraries can be used in sequence.

The way that you will insert Redux Middleware is via the applyMiddleware() function which is passed as a parameter to the createStore() function.

REDUX THUNK

Redux Thunk is one of the most useful redux middleware libraries.

thunk“: a programming technique where you wrap a function inside another function to delay its execution until it is needed!

This technique is used in Redux Thunk to inject extra operations into an action creator. Action creators normally return an action (object).

Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met or even to dispatch multiple actions.

So normally the way that Redux is set up, the action creator function is expected to return an object, which is sent directly to the reducer. Redux thunk enables you to use that action creator to delay stop or change that dispatch. This will hopefully make more sense once you are using it.

In our project we will use Redux Thunk to have an action creator function generate an asynchronous request to a server for data then dispatch a new action depending on the response to that request once it’s received.

Alternatives to Redux Thunk

Redux Thunk is the most common way to handle the setup of simple asynchronous logic in Redux action creators.

There are other ways such as: redux-saga, redux-observable


Exercise: Redux Thunk

Install Redux Thunk and Logger as shown below, with yarn add:

yarn add redux-thunk@2.3.0
yarn add redux-logger@3.0.6

 

configureStore.js (update it to use Thunk and Logger as follows)

import {createStore, combineReducers, applyMiddleware } from ‘redux’;
import thunk from ‘redux-thunk’;
import logger from ‘redux-logger’;
import { Campsites } from ‘./campsites’;
import { Comments } from ‘./comments’;
import { Partners } from ‘./partners’;
import { Promotions } from ‘./promotions’;
export const ConfigureStore = () => {
    const store = createStore(
        combineReducers({
            campsites: Campsites,
            comments: Comments,
            partners: Partners,
            promotions: Promotions
        }),
        applyMiddleware(thunk, logger)
    );
    return store;
};

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

 

ActionCreators.js

import * as ActionTypes from ‘./ActionTypes’;
import { CAMPSITES } from ‘../shared/campsites’;
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());
    setTimeout(() => {
        dispatch(addCampsites(CAMPSITES));
    }, 2000);
};
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
});

 

campsites.js

import * as ActionTypes from './ActionTypes';

export const Campsites = (state = {
        isLoading: true,
        errMess: null,
        campsites: []
    }, action) => {
    switch (action.type) {
        case ActionTypes.ADD_CAMPSITES:
            return {...state, isLoading: false, errMess: null, campsites: action.payload};
        case ActionTypes.CAMPSITES_LOADING:
            return {...state, isLoading: true, errMess: null, campsites: []};
        case ActionTypes.CAMPSITES_FAILED:
            return {...state, isLoading: false, errMess: action.payload};
        default:
            return state;
    }
};

LoadingComponent.js

import React from 'react';

export const Loading = () => {
    return (
        <div className="col">
            <i className="fa fa-spinner fa-pulse fa-3x fa-fw text-primary" />
            <p>Loading...</p>
        </div>
    );
};

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 { addComment, fetchCampsites } 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())
};
class Main extends Component {
    componentDidMount() {
        this.props.fetchCampsites();
    }
    
    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.filter(promotion => promotion.featured)[0]}
                    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.filter(comment => comment.campsiteId === +match.params.campsiteId)}
                    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’ component={Contact} />
                    <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’;
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={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;

HomeComponent.js

import React from ‘react’;
import { Card, CardImg, CardText, CardBody, CardTitle } from ‘reactstrap’;
import { Loading } from ‘./LoadingComponent’;
function RenderCard({item, isLoading, errMess}) {
    if (isLoading) {
        return <Loading />;
    }
    if (errMess) {
        return <h4>{errMess}</h4>;
    }
    return (
        <Card>
            <CardImg src={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} />
                </div>
                <div className=”col-md m-1″>
                    <RenderCard item={props.partner} />
                </div>
            </div>
        </div>
    );
}
export default Home;

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’;
function RenderDirectoryItem({campsite}) {
    return (
        <Card>
            <Link to={`/directory/${campsite.id}`}>
                <CardImg width=”100%” src={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;

Exercise: React-Redux-Form Revisited

Use react-redux-form to interact with Redux store and store the state of the form in the store.

Add a new file named forms.js in redux folder
export const InitialFeedback = {
    firstName: '',
    lastName: '',
    phoneNum: '',
    email: '',
    agree: false,
    contactType: 'Pho
ne',
    feedback: ''
}; configureStore.js
import {createStore, combineReducers, applyMiddleware } from ‘redux’;
import { createForms } from ‘react-redux-form’;
import thunk from ‘redux-thunk’;
import logger from ‘redux-logger’;
import { Campsites } from ‘./campsites’;
import { Comments } from ‘./comments’;
import { Partners } from ‘./partners’;
import { Promotions } from ‘./promotions’;
import { InitialFeedback } from ‘./forms’;
export const ConfigureStore = () => {
    const store = createStore(
        combineReducers({
            campsites: Campsites,
            comments: Comments,
            partners: Partners,
            promotions: Promotions,
            …createForms({
                feedbackForm: InitialFeedback
            })
        }),
        applyMiddleware(thunk, logger)
    );
    return store;
};

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 { addComment, fetchCampsites } 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’))
};
class Main extends Component {
    componentDidMount() {
        this.props.fetchCampsites();
    }
    
    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.filter(promotion => promotion.featured)[0]}
                    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.filter(comment => comment.campsiteId === +match.params.campsiteId)}
                    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));
ContactComponent.js
import React, { Component } from ‘react’;
import { Breadcrumb, BreadcrumbItem, Button, Label, Col, Row } from ‘reactstrap’;
import { Link } from ‘react-router-dom’;
import { Control, Form, Errors } from ‘react-redux-form’;
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 Contact extends Component {
    constructor(props) {
        super(props);
        this.state = {
            firstName: ”,
            lastName: ”,
            phoneNum: ”,
            email: ”,
            agree: false,
            contactType: ‘By Phone’,
            feedback: ”,
            touched: {
                firstName: false,
                lastName: false,
                phoneNum: false,
                email: false
            }
        };
        this.handleSubmit = this.handleSubmit.bind(this);
    }
    handleSubmit(values) {
        console.log(‘Current state is: ‘ + JSON.stringify(values));
        alert(‘Current state is: ‘ + JSON.stringify(values));
        this.props.resetFeedbackForm();
    }
    render() {  
        return (
            <div className=”container”>
                <div className=”row”>
                    <div className=”col”>
                        <Breadcrumb>
                            <BreadcrumbItem><Link to=”/home”>Home</Link></BreadcrumbItem>
                            <BreadcrumbItem active>Contact Us</BreadcrumbItem>
                        </Breadcrumb>
                        <h2>Contact Us</h2>
                        <hr />
                    </div>
                </div>
                <div className=”row row-content align-items-center”>
                    <div className=”col-sm-4″>
                        <h5>Our Address</h5>
                        <address>
                            1 Nucamp Way<br />
                            Seattle, WA 98001<br />
                            U.S.A.
                        </address>
                    </div>
                    <div className=”col”>
                        <a role=”button” className=”btn btn-link” href=”tel:+12065551234″><i className=”fa fa-phone” /> 1-206-555-1234</a><br />
                        <a role=”button” className=”btn btn-link” href=”mailto:fakeemail@fakeemail.co”><i className=”fa fa-envelope-o” /> campsites@nucamp.co</a>
                    </div>
                </div>
                <div className=”row row-content”>
                    <div className=”col-12″>
                        <h2>Send us your Feedback</h2>
                        <hr />
                    </div>
                    <div className=”col-md-10″>
                        <Form model=”feedbackForm” onSubmit={values => this.handleSubmit(values)}>
                            <Row className=”form-group”>
                                <Label htmlFor=”firstName” md={2}>First Name</Label>
                                <Col md={10}>
                                    <Control.text model=”.firstName” id=”firstName” name=”firstName”
                                        placeholder=”First 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’
                                        }}
                                    />
                                </Col>
                            </Row>
                            <Row className=”form-group”>
                                <Label htmlFor=”lastName” md={2}>Last Name</Label>
                                <Col md={10}>
                                    <Control.text model=”.lastName” id=”lastName” name=”lastName”
                                        placeholder=”Last Name”
                                        className=”form-control”
                                        validators={{
                                            required,
                                            minLength: minLength(2),
                                            maxLength: maxLength(15)
                                        }}
                                    />
                                    <Errors
                                        className=”text-danger”
                                        model=”.lastName”
                                        show=”touched”
                                        component=”div”
                                        messages={{
                                            required: ‘Required’,
                                            minLength: ‘Must be at least 2 characters’,
                                            maxLength: ‘Must be 15 characters or less’
                                        }}
                                    />
                                </Col>
                            </Row>
                            <Row className=”form-group”>
                                <Label htmlFor=”phoneNum” md={2}>Phone</Label>
                                <Col md={10}>
                                    <Control.text model=”.phoneNum” id=”phoneNum” name=”phoneNum”
                                        placeholder=”Phone number”
                                        className=”form-control”
                                        validators={{
                                            required,
                                            minLength: minLength(10),
                                            maxLength: maxLength(15),
                                            isNumber
                                        }}
                                    />
                                    <Errors
                                        className=”text-danger”
                                        model=”.phoneNum”
                                        show=”touched”
                                        component=”div”
                                        messages={{
                                            required: ‘Required’,
                                            minLength: ‘Must be at least 10 numbers’,
                                            maxLength: ‘Must be 15 numbers or less’,
                                            isNumber: ‘Must be a number’
                                        }}
                                    />
                                </Col>
                            </Row>
                            <Row className=”form-group”>
                                <Label htmlFor=”email” md={2}>Email</Label>
                                <Col md={10}>
                                    <Control.text model=”.email” id=”email” name=”email”
                                        placeholder=”Email”
                                        className=”form-control”
                                        validators={{
                                            required,
                                            validEmail
                                        }}
                                    />
                                    <Errors
                                        className=”text-danger”
                                        model=”.email”
                                        show=”touched”
                                        component=”div”
                                        messages={{
                                            required: ‘Required’,
                                            validEmail: ‘Invalid email address’
                                        }}
                                    />
                                </Col>
                            </Row>
                            <Row className=”form-group”>
                                <Col md={{size: 4, offset: 2}}>
                                    <div className=”form-check”>
                                        <Label check>
                                            <Control.checkbox
                                                model=”.agree”
                                                name=”agree”
                                                className=”form-check-input”
                                            /> {‘ ‘}
                                            <strong>May we contact you?</strong>
                                        </Label>
                                    </div>
                                </Col>
                                <Col md={4}>
                                    <Control.select model=”.contactType” name=”contactType”
                                        className=”form-control”>
                                        <option>By Phone</option>
                                        <option>By Email</option>
                                    </Control.select>
                                </Col>
                            </Row>
                            <Row className=”form-group”>
                                <Label htmlFor=”feedback” md={2}>Your Feedback</Label>
                                <Col md={10}>
                                    <Control.textarea model=”.feedback” id=”feedback” name=”feedback”
                                        rows=”12″
                                        className=”form-control”
                                    />
                                </Col>
                            </Row>
                            <Row className=”form-group”>
                                <Col md={{size: 10, offset: 2}}>
                                    <Button type=”submit” color=”primary”>
                                        Send Feedback
                                    </Button>
                                </Col>
                            </Row>
                        </Form>
                    </div>
                </div>
            </div>
        );
    }
}
export default Contact;


Additional Resources:

You May Also Like