Bootcamp Notes – Day 13 (Wed) – React: Week 4 – Introduction to Redux

Introduction to Redux

The MVC Design Pattern

In software development you will often read or hear people mention something called MVC or one of it’s variants such as MVVM, or MVA. You may also sometimes hear it called the MVC framework (not a framework like Bootstrap). But it is not a framework in the sense that Bootstrap or Angular is a framework. Instead, MVC is what as known as a software design pattern. It is not a library, it doesn’t consist of any actual code at all. What it is is a reusable approach for how to structure an applications code. The MVC design pattern was formally introduced back in the 70’s, for use with with developing user interfaces for desktop applications. Typically MVC is used to develop presentation layer/client-side. It is not a pattern that’s used for the entire application, but for developing the presentation layer of the application the UI. Since then it has become very popular in web development as well. Today, popularly used in web applications.

A key concept of MVC lies in it’s separation of concerns. Separation of concerns in software development facilitates independent development, testing, maintenance of different parts of an app. Not to mention independent testing and maintenance. With MVC, the presentation layer is divided into three parts: Model, View, Controller 

  • MODEL: handle application state and logic
  • VIEW: present information to user
  • CONTROLLER: mediate between model and view

  • The model stores the main application logic and data for the front end. Think of it as not a database but the data that’s been loaded into the application from the database and the front-end code that sends request to and receives data from the database. It will also respond to queries from the front end such as queries about its state, which usually come from the view and it will respond to request to change its state which come from the controller. It may also be set up to automatically send out notifications to the view or controller when any changes are made to it
  • The view renders the model into a form suitable for display. Typically a UI element. The view can query the model directly for information about the application state, so that it can adjust itself.
  • The controller receives information from the view, such as if a user submits data through an input element. Then it can process and relay that that information to the model and instruct the model to change it’s state. It can also send instructions to the view to make changes.

The the MVC pattern is very common in programming and you should know about it, React does NOT use MVC!

React state management is often handled with a newer design pattern called Flux.

We will be using a state management library called Redux, which owes some of its parentage to Flux.


The Flux Architecture

Initially, React developers (at Facebook) used React with the MVC pattern, especially for creating the View. But, they found that MVC did not scale well for a complex, large application like Facebook, with many thousands of components. MVC became unpredictable at scale, difficult to add new features without causing unexpected cascading effects in other parts of the code: bugs. Largely due to the bi-directional design of MVC data flow.

You can see in this diagram the complexity of MVC. Once small change in the app could cause the whole thing to bug crazy!

So the engineers at Facebook came up with a new design pattern or architecture that they call FLUX! Like MVC, it is an approach on how to structure on the front end. It can be considered an evolution of MVC. The key feature of Flux that sets it apart from MVC is: It has one way data flow.

It separates into action, dispatcher, store, view

  • Once central dispatcher, multiple stores
  • One Dispatcher acts as traffic controller, routes actions to the appropriate stores
  • Stores hold application state and logic. There can be multiple stores.
  • Views update when stores are changed.

Redux a popular JavaScript state management library influenced by FLUX!


Overview: Redux

Flux is an architecture/design pattern that describes an approach to structuring an application’s front end.

A design pattern does not provide actual code.

JavaScript libraries that implement Flux include: Flummox, Alt, Redux

Redux has some differences from FLUX but is inspired/influenced by FLUX and can be considered an evolution. Created by Dan Abramov for use with React, co-written with Andrew Clark, author of Flummox. Aside from FLUX, also inspired/influenced by: the ELM functional programming language and three JavaScript libraries: Immutable.js, Baobab.js, RxJS

Redux is a “predictable state container for JavaScript apps.”

Provides code that stores application state and a consistent way to access and update that state from anywhere in the application. Not just for React – can be used with any JavaScript application. Redux could be used with Vanilla JavaScript, with backbone.js and amber.js

Now in our course project application which is quite simple: the container component Main stores state and passes data as props to several presentational components; changes to state only occur in Main. In the real world, we have have 100’s or 1000’s of components with more complicated hierarchies and groupings. State management becomes more tricky at scale; Redux handles predictable, consistent state management.

Redux is very helpful at scale!

FUNDALMENTAL PRINCIPLES OF REDUX

There are three fundamental principles of Redux:

  1. Single Source of Truth
  2. State is Read-Only
  3. Changes are made with Pure Functions

 

Single Source of Truth

FLUX: There can be multiple stores; dispatcher directs actions to the right one.

Redux: Only one store; holds a single state object tree that contains the whole state of your application as objects within that tree.

Object tree: Tree-like data structure; each node contains an object; DOM is another example of an object tree.

State is read only

Only way to change state is with an action – an object that describes what happened

 

CHANGES ARE MADE WITH PURE FUNCTIONS

Changes are made with pure functions called reducers.

Pure functions are functions that have no side effects and given the same inputs, always returns the same outputs.

Example: Math.floor() is a pure function – given a number, always gives same result back, does nothing else; Math.random() is an impure function – will always give a different output.

A Redux reducer is a pure function that takes the previous state and an action and returns the next state.

Similar in concept to array methods like map that return a new array and do not mutate the original – Reducers always return new state objects instead of mutating the previous state.

Programming concept: immutability — data does not get mutated, only replaced by new data

Now replacing the entire state every time you need to make the smallest change might seem like overkill, but objects are cheap to create, and this approach creates advantages such a “time travel” – able to step back and forward from previous state.

Now, think of it like frames in an animation sequence or a flipbook – instead of redrawing the frame/page, a new one is created with changes; you can flip back and forth!

Now let’s see REDUX IN PRACTICE

In the flux architecture we had actions, dispatcher, store and view.

In Redux we have: actions, reducers, a single store, and the view

The dispatcher in FLUX worked as a traffic controller that routed actions to different stores with only one store that is no longer a major role. But we still have a dispatch method in Redux. User interactions with the view trigger the dispatch of actions. The actions are sent to reducers and reducers create a new state, which is inside the store. Then the view will change based on the state. As in FLUX this is strictly a one-way data flow. Reducers don’t talk back to actions and the stores does not talk back to the reducers. Again, the data flows in one direction.

React-Redux-Library

To use Redux with React, you will install the react-redux-library. From this library will cover a few important functions that you will learn to use later:

createStore() – Creates the Redux store which holds the state (as an object tree)

connect() – Generates container component that wraps around other components to subscribe them to the store.

mapStateToProps() – Passed a callback to connect() function. Called whenever state changes. Receives entire state tree & returns an object that contains only the data needed by the component.

Will also use a React Redux component named <Provider>: Which wraps around the root component of the app. Takes the store variable as an attribute. Makes store accessible to all child components that are connected using connect() function.


Exercise: Introduction to Redux

  • First, install Redux and React-Redux into your application as follows, with Yarn:
yarn add redux@4.0.5
yarn add react-redux@7.2.0
  • Next, create a folder named redux inside the src folder.
  • Inside the src/redux folder, add a file named reducer.js with the code below:
import { CAMPSITES } from '../shared/campsites';
import { COMMENTS } from '../shared/comments';
import { PARTNERS } from '../shared/partners';
import { PROMOTIONS } from '../shared/promotions';

export const initialState = {
    campsites: CAMPSITES,
    comments: COMMENTS,
    partners: PARTNERS,
    promotions: PROMOTIONS
};

export const Reducer = (state = initialState, action) => {
    return state;
};
  • Then, add a file named configureStore.js in the redux folder and add the following code to it:
import { createStore } from 'redux';
import { Reducer, initialState } from './reducer';

export const ConfigureStore = () => {
    const store = createStore(
        Reducer,
        initialState
    );

    return store;
};

 

App.js

import React, { Component } from ‘react’;
import Main from ‘./components/MainComponent’;
import { BrowserRouter } from ‘react-router-dom’;
import { Provider } from ‘react-redux’;
import { ConfigureStore } from ‘./redux/configureStore’;
import ‘./App.css’;
const store = ConfigureStore();
class App extends Component {
    render() {
        return (
            <Provider store={store}>
                <BrowserRouter>
                    <div className=”App”>
                        <Main />
                    </div>
                </BrowserRouter>
            </Provider>
        );
    };
}
export default App;

 

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’;
const mapStateToProps = state => {
    return {
        campsites: state.campsites,
        comments: state.comments,
        partners: state.partners,
        promotions: state.promotions
    };
};
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)}
                />
            );
        };    
        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)(Main));

Exercise: React Redux Form

Install:  yarn add react-redux-form@1.16.14
ContactComponent.js
import React, { Component } from ‘react’;
import { Breadcrumb, BreadcrumbItem, Button, Label, Col, Row } from ‘reactstrap’;
import { Link } from ‘react-router-dom’;
import { Control, LocalForm } from ‘react-redux-form’;
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));
    }
    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″>
                        <LocalForm 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”
                                    />
                                </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”
                                    />
                                </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”
                                    />
                                </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”
                                    />
                                </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>
                        </LocalForm>
                    </div>
                </div>
            </div>
        );
    }
}
export default Contact;

Exercise: React Redux Form Validation

ContactComponent.js
import React, { Component } from ‘react’;
import { Breadcrumb, BreadcrumbItem, Button, Label, Col, Row } 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 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));
    }
    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″>
                        <LocalForm 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>
                        </LocalForm>
                    </div>
                </div>
            </div>
        );
    }
}
export default Contact;

 


Additional Resources: