SteveSunTech / ReactBlog

React | Redux | React-Router | Redux-Form | Backend API

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

React Blog

demo

Getting Started

Checkout this repo, install dependencies, then start the gulp process with the following:

> git clone https://github.com/WeiChienHsu/WeatherReact.git
> cd WeatherReact
> npm install
> npm start

Features

  • Save and Retrive the Posts
  • Navigate Users (React Router)
  • Add Different Page
  • Load Data from backend API based on user current Route
  • Validation Form to POST data to a remote server

How to Build this APP

Redux Blog Post API Reference

Post API

  • POST Request API
POST Method
https://reduxblog.herokuapp.com/api/posts
{
    "title": "Hi!",
    "categories": "Kevin",
    "content": "Kevin"
}
  • GET Request API
GET Method
https://reduxblog.herokuapp.com/api/posts
[
  {
      "id": 216869,
      "title": "Hi!",
      "categories": "Kevin",
      "content": "Kevin"
  },
  {
      "id": 216868,
      "title": "Hi!",
      "categories": "Kevin",
      "content": "Kevin"
  },
]

React Router (Single Page Application)

No longer navigating between distinct HTML documents that are being created by some remote web server. Instead, we always deal with "single HTML document" and rely on the JavaScript code to chagne the set of components that a user sees apprearing on the screen. We're tracking the user and showing them different sets of components.

  • Manage the URL and our appliaction.
npm install --save react-router-dom@4.0.0

history

  • User clicks on some links to change the URL inside their browser.
  • Browser said to the History Library and History wil run behind the server for us. To do some oarsing over it and figures out exactly what changed about the URL and pass it to React Router Library.
  • React Router receive the new Route and updates the react component shown on the screen.
  • React will rerender all components based on the new component.

BrowserRouter

Object is what interacts with the history library and decides exactly what to do based on the change of URL

Route

React Component that we can render inside of any other React Component inside our app (configuraion)

Render into ReactDOM

  • Create Two Components to show how to use React Router
class Hello extends React.Component{
  render(){return <div>Hello!</div>}
}

class GoodBye extends React.Component{
  render(){return <div>GoodBye!</div>}
}
ReactDOM.render(
  <Provider store={createStoreWithMiddleware(reducers)}>
    <BrowserRouter>
      <div>
        <Route path = "/hello" component = { Hello } />
        <Route path = "/goodbye" component = { GoodBye}/>
      </div>
    </BrowserRouter>
  </Provider>
  , document.querySelector('.container'));

Root Route of App

  • What will be showed for users
<Route path = "/" component = { PostIndex}>

Route for Patricular Posts of App

  • Need to set up route configuration: wildcard -> colin id
<Route path = "/posts/:id" component = { PostsShow }>

Route for Create Brand New Post

<Route path = "/posts/new" component = { PostsNew }>

Delete App component

  • Now we had a React Router, no longer need this app component anymore.

State as an Object

Show Pages

  • ActivePost is really a "duplicate piecce of state". Since the current route that a user is looking at is really another piece of state inside our application. Also, the ID that is reflected inside of that route or this particular route provides the exact same information as is being provided by ActivePost.
  • Since we've already used :id to reflect the current state, we just need a state to handle the posts.
  • When we had a URL, go back to see on the posts with that ID out of the list and show on the screen for users.
  • Save as an Object -> key: id of Post / value: post itself : make it easy to find the post we fetched
Easy to look up:
state.posts[postId]

Show Pages

Index Action

Action Creator

  • To fetch a list of posts and serve them up to our post index component

Axios & Redux Promise

  • Making Network Requests inside of an action creator.
  • Handle asynchronous nature of the request itself.
  • allpy Middleware "promise" intp index file
const createStoreWithMiddleware = applyMiddleware(promise)(createStore);

Build up a Request URL

  • Given a ROOT_URL
  • Given a Unique API_KEY
  • Get a Request by axios
import axios from 'axios';
export FETCH_POSTS = 'fetch_posts';

const ROOT_URL = 'https://reduxblog.herokuapp.com/api';
const API_KEY = '?key=weichien';

export function fetchPosts() {
  const request = axios.get(`${ROOT_URL}/posts${API_KEY}`);

  return {
    type: FETCH_POSTS,
    payload: request 
  }
}

Post Reducer

  • Produce the post piece of state to reutrn an Object
const rootReducer = combineReducers({
    posts: PostsReducer
});
  • Default the state as an Object
  • Need a loadash libraries to take the array and create an Object

_.mapKeys

  • First Argument : an Array
  • Second Argument : Property that we want to poll off of each object in here to use as the key on the resulting Objects.
const posts = [
  { id: 4, title: "Hi"},
  { id: 5, title: "Hi There!"},
  { id: 33, title: "Hi Guy!"}
 ]
_.mapKeys(posts,'id')
  • Output will be likes
{"4":{"id":4,"title":"Hi"},"5":{"id":5,"title":"Hi There!"},"33":{"id":33,"title":"Hi Guy!"}}
  • Simple ID Lookup
const state = _.mapKeys(posts,'id')
state["4"]

Posts Reducer

import { FETCH_POSTS} from '../actions';
import _ from 'lodash';

export default function(state = {}, action) {
  switch(action.type) {
    case FETCH_POSTS:
      return _.mapKeys(action.payload.data, 'id');
     
    default:
      return state;  

  }
}

Connect Action Creator with Post Index Component

  • Directly Passing Action Creator itself instead of connectint funciton "mapDispatchToProp"
  • mapDispatchToProp: Separate function format when we want to use a seperate function
export default connect(null, {fetchPosts : fetchPosts})(PostsIndex);

When to reach out the API

  • Event: when are we going to attempt to reach out to the API and fetch our list of Posts?

ComponentDidMount

Will be called by React immedicately when the component was created and showed on the DOM

  • Great location to go and fetch some data or initiate some one time loading procedure.
  componentDidMount() {
    this.props.fetchPosts();
  }

Fetchig data is an asynchronous operation.

Take some times to take the data. React wouldn't automatically wait till the data was took, so we need some React lifecylec methods to help.

Manually Create a post to test

{
    "title": "Kevin",
    "categories": "Fun",
    "content": "Great Fun Stories!"
}
  • Then, we can see in Google Developer Tool, Network category, there are some XHR requests.

NetWork

Hoop up Application level State

  • Everytime we need to use application level state, we need "mapStateToProps" function
function mapStateToProps(state) {
  return { posts : state.posts } 
}

export default connect(mapStateToProps, { fetchPosts })(PostsIndex);
  • Console.log:
218163:
categories:"Fun"
content:"Great Fun Stories!"
id:218163
title:"Kevin"

renderPosts()

  • Since we request to an Object that contains a list of Posts, when we look at this.props.posts, it was an Object and we don't get a helper method to map it!
  • Map over an Object and return an array -> loadash
  renderPosts() {
    return _.map(this.props.posts, post => {
      return(
        <li className = "list-group-item" key = {post.id}>
          {post.title}
        </li> 
      )
    })
  }

Render the renderPosts in

          <div>
             <h3>Posts</h3>
             <ul className = "list-group">
              {this.renderPosts()}
             </ul>
          </div>

    Create New Posts

    NewPosts

      1. Scaffold "PostsNew" component
      1. Add Route configuration to show PostsNew component (/posts/new)
      1. Add navigation between Index and New (Link between each Pages)
      1. Add Form to PostNew
      1. Make Action Creator to "Save Post" when use click 'Create', make sure it will be save to Backend

    Route Configuration

    • Wrong about Route defination : When React Router see a "/", it will render a PostsIndex Component. But when "/posts/new" also have slash, it still show the PostsIndex
    • Not like the traditional Routing as Django or Express
      <Route path = "/" component = { PostsIndex } />
      <Route path = "/posts/new" component = { PostsNew } />

    Switch Component

    • Import the Swithch Component from 'react-router-dom'
    • Put the most sepcific Path in the top
    <Switch> 
      <Route path = "/posts/new" component = { PostsNew } />
      <Route path = "/" component = { PostsIndex } />
    </Switch>

    Navigation with the Link Component

    • In React Router, we just tell it to show a new set of components, not use anchor tags to do discrete navigation between different routes inside the browser.

    Link Component

    • When click on a link tag, it has a couple of event handlers on it that prevent the browser to do what it normal did. (ex issue another Http Request to fetch other HTML documents from server)
    <div className = "text-xs-right">
      <Link className = "btn btn-primary"  to = "/posts/new">
        Add a Post
      </Link>
    </div>

    Redux Form

    • Need to validate whatever the user puts into each of these inputs.
    • Redux from - Handle any type of form that we put together with redux validating the input and then submitting the form in some fashion.

    Redux Form

    • Provide all kinds of examples for all Form!
    npm install --save redux-form@6.6.3
    
    • Redux Form uses our Redux instances for handling of the state that is being produced by the form like the actual form that's getting rendered on the screen

    formReducer

    • The form Reducer is being applied to the form piece of state.
    import { reducer as formReducer} from 'redux-form';
    
    const rootReducer = combineReducers({
        posts: PostsReducer,
        form: formReducer
    });

    How we use the Redux Form

    Redux Form

    1. Identify Three differents piece of States we had(Title, Categories, Contents)
    2. Create a Firld Component by React Form. We need to tell what kinds of Inputs we would like to receive from users.(ex Check box)
    3. User changes a 'Field' input
    4. Redux automatically handles those changes for us. (Doing the onClick function, set the states etc. for us) Handle Any Changes!!!
    5. User Submit the form
    6. Two Callbacks to React Form: validate inputs and handle from submittal.(React Form: OK, the user's inputs data is valid and go take these data to whatever you like to do)

    Field Component

    • Import Two Helpers from redux-form in PostsNew Component
    • reduxForm: Allow Component to communicate with reduxReducer (Just like Connect Helper)
    import { Field, reduxForm } from 'redux-form';
    
    export default reduxForm({
        form : 'PostsNewForm'
    })(PostsNew);

    Pass in some props into Field

    • name: What piece of states we're going to use
    • component : Take a function or Component
      <Field
        name = "title"
        component = {this.renderTitleField}
      />

    Component props inside Field Component

    Helper Function

    • Redux Form function Helper : JSX blob
    • In the renderTitleField, filed argument contains EventHandler and Props for making sure the Field know it will contact with "title"
      renderTitleField(field) {
        return(
          <div>
              <input 
                type = "text"
                {...field.input}
              />
          </div>
        )
      }

    Generalizing Fields

    • Add additional Styling to format that input more nicely.
    <div className = "form-group">
      <label>Title</label>
        <input 
          className = "form-control"
    • Generalizing Other Render Function together by passing arbitrary propseties(label) into Field
    • Used another Label attribute!
    <label>{field.label}</label>
    
    
    <Field
      label = "Title"
      name = "title"
      component = {this.renderField}
    />
    <Field
      label = "Tags"
      name = "tags"
      component = {this.renderField}
    />

    Submit Inputs and Validating Forms

    Validation - Automatically! (from Redux Form)

    • validate function: When User try to submit(press Enter) wil be called, values contain all the inpurs from users
    • -> {title: "abc", categories: "abc", content: "abc"}
    • Logic to validate inputs from 'values':
    • A variable Errors: If errors is empty, the form is fine to submit
    function validate(values) {
        const errors = {};
    
        if(!values.title || values.title.length < 3) {
          errors.title = "Please Enter a title as least 3 characters!";
        }
    
        if(!values.categories) {
          errors.categories = "Please Enter some catefories!";
        }
    
        if(!values.content) {
          errors.content = "Please Enter some content!";
        }
    
        return errors;
    }

    Showing Errors to Users

    • Used Errors Props in renderField function
      {field.meta.error}

    Handling Form Submittal

    • When Redux take information out of this form, the things it will do still depend on us.
    • We want to make sure when we click "onSubmit", validate function will be called automatically
    const { handleSubmit } = this.props;
    • This is a property that is being passed to the component on behalf of redux form.
    • Inside OnSubmit Function -> Since Redux form only handle states and validation, we need to handle other things by oursleves.
      onSubmit(values) {
          console.log(values);
      }
    
      render() {
        const { handleSubmit } = this.props;
    
        return(
          <form onSubmit = { handleSubmit(this.onSubmit.bind(this)) }>
    • We got the values passed out from Redux Form:
    {title: "adsf", categories: "asdf", content: "asdf"}

    Form and Field States

    Redux Form Works Internally (3 different States):

    • Pristine State: First Show on Screen (User havn't selected or inputed)
    • Touched State: User has selected or focused (When users are typing the input)
    • Invalid State: Show the Error Message
    // Need to show error message when user stats typing or submit
    
    {field.meta.touched ? field.meta.error : ''}

    Conditional Styling

    • Give a conditional Styling by declaring a className as variable
      renderField(field) {
        const className = `form group ${field.meta.touched && field.meta.error ? 'has-danger' : ''}`
        return(
          <div className = {className}>

    Navigation - Submit and back to Root Page

    • Link Component to root page
      <button type = "submit" className = "btn btn-primary"> Submit </button>
      <Link to = "/" className = "btn btn-danger">Cancel</Link>

    Post Action Creator - Save the Post on Backend

    • Action Creator: Make a request to our API with axios, and return an Action with Request as payload.
    export function createPost(values) {
        const request = axios.post(`${ROOT_URL}/posts${API_KEY}`, values);
        return {
          type: CREATE_POST,
          payload: request
        }
      }
    • Connect Action Creator(createPost) with PostsNew Component (Combine two helperes together - reduxForm/connect)
    export default reduxForm({
        validate,
        form : 'PostsNewForm'
    })(
      connect(null, { createPost })(PostsNew)
    );

    Result

    • Call action creator when we onSubmit
      onSubmit(values) {
          this.props.createPost(values);
      }

    Navigation Through Callbacks

    Navigation Grpah After User Submits, we like to redirect user back to List of Posts.

    1. User submits form
    2. Validate form
    3. Call 'onSubmit'
    4. Call an action creator to make API request
    5. Wait....
    6. After success, navigate the user to post list
    • Link tag is not appropriate now since it's not for programmatic navigation (It's respone to user clicks something)

    • We want to automatically navigate the user around our application

      onSubmit(values) {
          this.props.history.push('/');
          this.props.createPost(values);
      }
    • We Navigate too soon! Only Redirect when Post is successfuly Created by using Callback function
    • In PostsNew Component: Let history.push as a callback fun citon of createPost function
      onSubmit(values) {
          this.props.createPost(values, () => {
            this.props.history.push('/');
          });
      }
    • In Action Creator: Receive the callback funcion(history.push) and use the Promise. When Post request is successfully created, "then" execute the callback function
    export function createPost(values, callback) {
        const request = axios.post(`${ROOT_URL}/posts${API_KEY}`, values)
          .then(() => callback());
          
        return {
          type: CREATE_POST,
          payload: request
        }
      }

    Post Show Component

    • Create Post Show Component
    • Add a Routing between PostsNew and PostsIndex : Since if we put path = "/posts/:id" below path ="/posts/new/", new will be saw as an Id
      <Route path = "/posts/new" component = { PostsNew } />
      <Route path = "/posts/:id" component = { PostsShow } />
      <Route path = "/" component = { PostsIndex } />

    Receiving New Post - Action Creator

    • How to load data inside out Application: We only fetch the specific post that user want to see.
    • Action Creator to Fetch specific post
    export function fetchPost(id) {
      const request = axios.get(`${ROOT_URL}/posts/${id}${API_KEY}`);
      return {
        type: FETCH_POST,
        payload: request
      }

    Receiving New Post - Reducer

    • We Already have some number of posts possibly that have been fetched and stored inside this reducer.
    • Don't throw away all the different data that fetched over time.
    • Add to existing application level state rather than tossing away.
    switch(action.type) {
    
      case FETCH_POST:
      return { ...state, [action.payload.data.id] : action.payload.data };
    
      case FETCH_POSTS:
        return _.mapKeys(action.payload.data, 'id');

    Connect PostShow with PostReducer

    • When will we call that Action Creator: we place that data fetching action creator into a componet Did Mount Life Cycle Method!
      componentDidMount() {
        const { id } = this.props.match.params.id;
        this.props.fetchPost(id);
      }
    • We need to tell fetchPost which specific id we need to fetch.
    • Get access URL from React Router
     const { id } = this.props.match.params.id;
    • Send the object of list of Posts by using 'mapStateToProps' methods : ownProps -> is the prop going into current component
    • Return the Single Post we want
    function mapStateToProps({ posts }, ownProps) {
      return {post: posts[ownProps.match.params.id]};
    }

    Fix Error!! - Check id

    Uncaught TypeError: Cannot read property 'title' of undefined
    
    • We miss the first part of the whole flow. Our Component try to render on screen before we even attempt to fetch the post
    • We don't hae correct id sitting into the memory
       if(!post) {
          return <div> Loading ... </div>
        }
    • Dont forget to send "mapStateToProps" into connect helper method
    export default connect(mapStateToProps, { fetchPost })(PostsShow);

    Navigation between PostsNew and PostIndex

    • Link for specific Page
    <Link to = {`/posts/${post.id}`}>
      {post.title}
    </Link>

    Caching Records

    • Network usage
      componentDidMount() {
        if(!this.props.post) {
          const { id } = this.props.match.params;
          this.props.fetchPost(id);
        }
      }

    Deleting a Post

    • button with props
     <Link to = "/" className = "btn btn-primary"> Back To Index </Link>
              <button
                className = "btn btn-danger"
                onClick = {this.onDeleteClick.bind(this)}
              >
                Delete Post
              </button>
    • onDeleteClick Function
      onDeleteClick() {
        const { id } = this.props.match.params;
        this.props.deletePost(id);
      }

    Action Creator - deletePost()

    • Import and Add deletePost into connect helper in Post Show Component
    • When we delete the post, need to redirect user back to Index Page
      onDeleteClick() {
        const { id } = this.props.match.params;
        this.props.deletePost(id,() => {
          this.props.history.push('/');
        });
      }
    • We don't need to access the post anymore, really care about we really delete the post from application state, so we could only return the id the make sure
    export function deletePost(id, callback) {
      const request = axios.delete(`${ROOT_URL}/posts/${id}${API_KEY}`)
        .then(() => callback());
      
      return {
        type: DELETE_POST,
        payload: id
      }
    }

    Local State Managment

    • Handle the case when we're deleting the Post, to get rid of key and value
    • In PostsReducer, lodash omit
    export default function(state = {}, action) {
      switch(action.type) {
    
        case DELETE_POST:
          return _.omit(state, action.payload);

    About

    React | Redux | React-Router | Redux-Form | Backend API


    Languages

    Language:JavaScript 92.8%Language:HTML 5.0%Language:CSS 2.2%