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
- 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
- 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"
},
]
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
- 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.
Object is what interacts with the history library and decides exactly what to do based on the change of URL
React Component that we can render inside of any other React Component inside our app (configuraion)
- 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'));
- What will be showed for users
<Route path = "/" component = { PostIndex}>
- Need to set up route configuration: wildcard -> colin id
<Route path = "/posts/:id" component = { PostsShow }>
<Route path = "/posts/new" component = { PostsNew }>
- Now we had a React Router, no longer need this app component anymore.
- 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]
- To fetch a list of posts and serve them up to our post index component
- 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);
- 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
}
}
- 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
- 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"]
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;
}
}
- 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);
- Event: when are we going to attempt to reach out to the API and fetch our list of Posts?
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();
}
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.
- By using PostMan, make a POST Request to https://reduxblog.herokuapp.com/api/posts?key=weichien
{
"title": "Kevin",
"categories": "Fun",
"content": "Great Fun Stories!"
}
- Then, we can see in Google Developer Tool, Network category, there are some XHR requests.
- 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"
- 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>
)
})
}
<div>
<h3>Posts</h3>
<ul className = "list-group">
{this.renderPosts()}
</ul>
</div>
-
- Scaffold "PostsNew" component
-
- Add Route configuration to show PostsNew component (/posts/new)
-
- Add navigation between Index and New (Link between each Pages)
-
- Add Form to PostNew
-
- Make Action Creator to "Save Post" when use click 'Create', make sure it will be save to Backend
- 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 } />
- 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>
- 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.
- 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>
- 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.
- 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
- 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
});
- Identify Three differents piece of States we had(Title, Categories, Contents)
- 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)
- User changes a 'Field' input
- Redux automatically handles those changes for us. (Doing the onClick function, set the states etc. for us) Handle Any Changes!!!
- User Submit the form
- 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)
- 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);
- name: What piece of states we're going to use
- component : Take a function or Component
<Field
name = "title"
component = {this.renderTitleField}
/>
- 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>
)
}
- 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}
/>
- 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;
}
- Used Errors Props in renderField function
{field.meta.error}
- 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"}
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 : ''}
- 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}>
- Link Component to root page
<button type = "submit" className = "btn btn-primary"> Submit </button>
<Link to = "/" className = "btn btn-danger">Cancel</Link>
- 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)
);
- Call action creator when we onSubmit
onSubmit(values) {
this.props.createPost(values);
}
After User Submits, we like to redirect user back to List of Posts.
- User submits form
- Validate form
- Call 'onSubmit'
- Call an action creator to make API request
- Wait....
- 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
}
}
- 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 } />
- 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
}
- 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');
- 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]};
}
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);
- Link for specific Page
<Link to = {`/posts/${post.id}`}>
{post.title}
</Link>
- Network usage
componentDidMount() {
if(!this.props.post) {
const { id } = this.props.match.params;
this.props.fetchPost(id);
}
}
- 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);
}
- 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
}
}
- 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);