Modern React with Redux
[TOC]
Section 6: Understanding Lifecycle Methods A code refactor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const SeasonDisplay = (props ) => { let season = getSeason(props.lat, new Date ().getMonth()); let text = season === 'winter' ? 'it is chilly' : "let's hit beach" ; let icon = season === 'winter' ? 'snowflake' : 'sun' ; return ( <h1> <i className={`${icon} icon` } /> <div>{text}</div> <i className={`${icon} icon` } /> </h1> ); }; const getSeason = (lat, month ) => { if (month > 2 && month < 9 ) { return lat >= 0 ? 'summer' : 'winter' ; } else { return lat < 0 ? 'summer' : 'winter' ; } }; export default SeasonDisplay;
to
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const seasonConfig = { summer: { text: "let's hit beach" , iconName: 'sun' , }, winter: { text: 'it is chilly' , iconName: 'snowflake' , } }; const SeasonDisplay = (props ) => { const season = getSeason(props.lat, new Date ().getMonth()); const { text, iconName } = seasonConfig[season]; return ( <h1> <i className={`${iconName} icon` } /> <div>{text}</div> <i className={`${iconName} icon` } /> </h1> ); }; const getSeason = (lat, month ) => { if (month > 2 && month < 9 ) { return lat >= 0 ? 'summer' : 'winter' ; } else { return lat < 0 ? 'summer' : 'winter' ; } }; export default SeasonDisplay;
Use configuration variable(object) to replace if statement
Set default props e.g. in a loader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import React from 'react' ;const Loader = (props ) => { return ( <div className="ui active dimmer" > <div className="ui big text loader" >{props.message}</div> </div> ); }; Loader.defaultProps = { message: 'Loading...' , }; export default Loader;
or set default value generally
1 2 3 4 5 6 7 8 9 10 11 import React from 'react' ;const Loader = (props ) => { return ( <div className="ui active dimmer" > <div className="ui big text loader" >{props.message || 'Loading...' }</div> </div> ); }; export default Loader;
Use helper method to render conditional content 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 renderContent ( ) { if (this .state.errorMessage && !this .state.lat) { return <div > Error: {this.state.errorMessage}</div > ; } if (!this .state.errorMessage && this .state.lat) { return <SeasonDisplay lat ={this.state.lat} /> ; } return <Loader message ={ 'Please accept the location prompt '} /> ; } render ( ) { return <div className ="border red" > {this.renderContent()}</div > ; }
otherwise we need to decide the situation in render() with if statement
Component Lifecycle
Event Handler
Uncontrolled Event Handler (React don’t know the input value, not recommended)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class SearchBar extends React .Component { onInputChange (event ) { console .log(event.target.value); } render ( ) { return ( <div className="ui segment" > <form className="ui form" > <div className="field" > <label>Image Search</label> <input type="text" onChange={this .onInputChange}/> </div> </form> </div> ) } }
This is the same as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class SearchBar extends React .Component { render ( ) { return ( <div className="ui segment" > <form className="ui form" > <div className="field" > <label>Image Search</label> <input type="text" onChange={e => console .log(e.target.value)} /> </div> </form> </div> ) } }
If we use onChange={this.onInputChange()}
, it means we call onInputChange function every time the component renders.
Controlled Event Handler(Recommend) React knows what is provided by checking state.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class SearchBar extends React .Component { state = { term: '' }; render ( ) { return ( <div className="ui segment" > <form className="ui form" > <div className="field" > <label>Image Search</label> <input type="text" value={this .state.term} onChange={e => this .setState({term : e.target.value})} /> </div> </form> </div> ) } }
“this” in JavaScript “this” doesn’t refer to the SearchBar component in this case
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class SearchBar extends React .Component { state = { term: '' }; onFormSubmit (e ) { e.preventDefault(); console .log(this .state.term); } render ( ) { return ( <div className="ui segment" > <form onSubmit={this .onFormSubmit} className="ui form" > <div className="field" > <label>Image Search</label> <input type="text" value={this .state.term} onChange={e => this .setState({term : e.target.value})} /> </div> </form> </div> ) } }
Let’s see some examples:
“this” refers to where the function is called
here, this === car object
Here car.drive
equals return this.sound
When we call truck.driveMyTruck()
, “this” refers to truck object
In this case, “this” is undefined when we call drive()
because drive()
= car.drive()
= return this.sound
. There is no “this” assigned, it is undefined.
So, “this” refers to who is calling it.
In the original case, <form onSubmit={this.onFormSubmit}
equals <form onSubmit={console.log(this.state.term)}
. So it doesn’t know what “this” refers to.
To solve this problem, we have three ways.
1. Bind the method to an object
In this case, we bind the this.drive
method to car object. So when we call drive()
, “this” in this.sound
equals to car object.
2. Arrow function (default solution) 1 2 3 4 onFormSubmit (e ) { e.preventDefault(); console .log(this .state.term); }
1 2 3 4 onFormSubmit: function (e ) { e.preventDefault(); console .log(this .state.term); }
The code above above are the same
While arrow function automatically bind “this” in its function. We can change the function above to
1 2 3 4 onFormSubmit = e => { e.preventDefault(); console .log(this .state.term); }
3. Arrow function again 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class SearchBar extends React .Component { state = { term: "" , }; onFormSubmit (e ) { e.preventDefault(); console .log(this .state.term); }; render ( ) { return ( <div className="ui segment" > <form onSubmit={(e ) => this .onFormSubmit(e)} className="ui form" > <div className="field" > <label>Image Search</label> <input type="text" value={this .state.term} onChange={(e ) => this .setState({ term : e.target.value })} /> </div> </form> </div> ); } }
Pass data from child component to parent - callback function as props App.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import React from "react" ;import axios from "axios" ;import SearchBar from "./SearchBar" ;class App extends React .Component { onSearchSubmit (term ) { axios.get("https://api.unsplash.com/search/photos" , { params: { query: term, }, headers: { Authorization: "Client-ID xxx" , }, }); } render ( ) { return ( <div className="ui container" style={{ marginTop : "10px" }}> <SearchBar onSubmit={this .onSearchSubmit} /> </div> ); } } export default App;
SearchBar.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import React from "react" ;class SearchBar extends React .Component { state = { term: "" , }; onFormSubmit = (e ) => { e.preventDefault(); this .props.onSubmit(this .state.term); }; render ( ) { return ( <div className="ui segment" > <form onSubmit={this .onFormSubmit} className="ui form" > <div className="field" > <label>Image Search</label> <input type="text" value={this .state.term} onChange={(e ) => this .setState({ term : e.target.value })} /> </div> </form> </div> ); } } export default SearchBar;
We pass a callback function this.onSearchSubmit
as a props to the SearchBar component. We can call this function in SearchBar component with data from itself as parameters.
Thus, the function from App component will be excuted with data from child component.
Section 8: Making API Request with React axios vs fetch axios: less code, prefer
Handle a promise 1. .then 1 2 3 4 5 6 7 8 9 10 11 12 13 onSearchSubmit (term ) { axios .get("https://api.unsplash.com/search/photos" , { params: { query: term, }, headers: { Authorization: "Client-ID LXjQUjgg4QYoH1QHhZCPuAro8C4CohztPyCoUkQc_d4" , }, }) .then((response ) => console .log(response.data.results)); }
2. async await 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async onSearchSubmit (term ) { const response = await axios.get( "https://api.unsplash.com/search/photos" , { params: { query: term, }, headers: { Authorization: "Client-ID LXjQUjgg4QYoH1QHhZCPuAro8C4CohztPyCoUkQc_d4" , }, } ); console .log(response.data.results); }
“this” problem again 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class App extends React .Component { state = { images : [] }; async onSearchSubmit (term ) { const response = await axios.get( "https://api.unsplash.com/search/photos" , { params: { query: term, }, headers: { Authorization: "Client-ID LXjQUjgg4QYoH1QHhZCPuAro8C4CohztPyCoUkQc_d4" , }, } ); # this.setState({ images: response.data.results }); } render ( ) { return ( <div className="ui container" style={{ marginTop : "10px" }}> <SearchBar onSubmit={this .onSearchSubmit} /> </div> ); } }
“this” in this.setState()
is the props object because this code in SearchBar.js
1 2 3 4 onFormSubmit = (e ) => { e.preventDefault(); this .props.onSubmit(this .state.term); };
we can refactor the code as below, automatically bind the “this” to App with arrow function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class App extends React .Component { state = { images : [] }; onSearchSubmit = async term => { const response = await axios.get( "https://api.unsplash.com/search/photos" , { params: { query: term, }, headers: { Authorization: "Client-ID LXjQUjgg4QYoH1QHhZCPuAro8C4CohztPyCoUkQc_d4" , }, } ); this .setState({ images : response.data.results }); }; render ( ) { return ( <div className="ui container" style={{ marginTop : "10px" }}> <SearchBar onSubmit={this .onSearchSubmit} /> <p>find {this .state.images.length} images</p> </div> ); } }
unsplash.js
1 2 3 4 5 6 7 8 9 import axios from "axios" ;export default axios.create({ baseURL: "https://api.unsplash.com" , headers: { Authorization: "Client-ID xxx" , } });
App.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import unsplash from "../api/unsplash" ;import SearchBar from "./SearchBar" ;class App extends React .Component { state = { images : [] }; onSearchSubmit = async term => { const response = await unsplash.get("/search/photos" , { params: { query: term, } }); this .setState({ images : response.data.results }); }; ...
Section 9: Building Lists of Records Use key to help React render list
1 2 3 4 5 6 7 8 9 10 11 12 const ImageList = props => { const images = props.images.map(image => { return ( <img key={image.id} src={image.urls.regular} alt={image.discription} /> ); }); return <div > {images}</div > ; };
Remember to add unique key for a list of items
Section10: Using Ref’s for DOM Access
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import React from "react" ;class ImageCard extends React .Component { constructor (props ) { super (props); this .state = { spans: 0 , }; this .imageRef = React.createRef(); } componentDidMount ( ) { this .imageRef.current.addEventListener("load" , this .setSpans); } setSpans = () => { const height = this .imageRef.current.clientHeight; const spans = Math .ceil(height / 10 ); this .setState({ spans : spans }); }; render ( ) { const { description, urls } = this .props.image; return ( <div style={{ gridRowEnd : `span ${this .state.spans} ` }}> <img ref={this .imageRef} alt={description} src={urls.regular} /> </div> ); } } export default ImageCard;
Use React.createRef()
to create a reference to the DOM element
change style according to the element
Section16: Redux
In a reducer, we always return a new data instead of modifying the previous one.
remove an element from list with filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 console .clear();const createPolicy = (name, amount ) => { return { type: 'CREATE_POLICY' , payload: { name: name, amount: amount } }; }; const deletePolicy = (name ) => { return { type: 'DELETE_POLICY' , payload: { name: name } }; }; const createClaim = (name, amountOfMoneyToCollect ) => { return { type: 'CREATE_CLAIM' , payload: { name: name, amountOfMoneyToCollect: amountOfMoneyToCollect } }; }; const claimsHistory = (oldListOfClaims = [], action ) => { if (action.type === 'CREATE_CLAIM' ) { return [...oldListOfClaims, action.payload]; } return oldListOfClaims; }; const accounting = (bagOfMoney = 100 , action ) => { if (action.type === 'CREATE_CLAIM' ) { return bagOfMoney - action.payload.amountOfMoneyToCollect; } else if (action.type === 'CREATE_POLICY' ) { return bagOfMoney + action.payload.amount; } return bagOfMoney; }; const policies = (listOfPolicies = [], action ) => { if (action.type === 'CREATE_POLICY' ) { return [...listOfPolicies, action.payload.name]; } else if (action.type === 'DELETE_POLICY' ) { return listOfPolicies.filter(name => name !== action.payload.name); } return listOfPolicies; }; const { createStore, combineReducers } = Redux;const ourDepartments = combineReducers({ accounting: accounting, claimsHistory: claimsHistory, policies: policies }); const store = createStore(ourDepartments);createPolicy('Alex' , 20 ) createClaim('Alex' , 120 ) deletePolicy('Alex' ) store.dispatch(createPolicy('Alex' , 20 )); store.dispatch(createPolicy('Jim' , 30 )); store.dispatch(createPolicy('Bob' , 40 )); store.dispatch(createClaim('Alex' , 120 )); store.dispatch(createClaim('Jim' , 50 )); store.dispatch(deletePolicy('Bob' )); console .log(store.getState());
Section 17: Integrating React with Redux Install 1 npm install redux react-redux
Structure Connect
that wraps the component(SongList) is responsible for fetching state from Store through Provider and pass actions to its component
Named export 1 2 3 4 5 6 export const selectSong = (song ) => { return { type: 'SONG_SELECTED' , payload: song }; };
named import
1 import {selectSong} from '../actions' ;
Invoke function of function connect()
returns a function, connect()()
calls this function
The process of Redux create action ->
pass action to all reducers with connect ->
reducers handle action and change global state ->
mapStateToProps received the changed global state and pass it to component props
An example:
actions-index.js
1 2 3 4 5 6 7 export const selectSong = song => { return { type: 'SONG_SELECTED' , payload: song, }; };
SongList.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import React from 'react' ;import { connect } from 'react-redux' ;import { selectSong } from '../actions' ;class SongList extends React .Component { renderList ( ) { return this .props.songs.map(song => ( <div className="item" key={song.title}> <div className="right floated content" > <button className="ui button primary" onClick={() => this .props.selectSong(song)}> Select </button> </div> <div className="content" >{song.title}</div> </div> )); } render ( ) { return <div className ="ui divided list" > {this.renderList()}</div > ; } } const mapStateToProps = state => { console .log(state); return { songs : state.songs }; }; export default connect(mapStateToProps, {selectSong})(SongList);
reducers - index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { combineReducers } from 'redux' ;const songsReducer = () => { return [ { title : 'No scrubs' , duration : '4:05' }, { title : 'macarena' , duration : '2:30' }, { title : 'all star' , duration : '3:15' }, { title : 'i want it that day' , duration : '1:45' }, ]; }; const selectedSongReducer = (selectedSong = null , action ) => { if (action.type === 'SONG_SELECTED' ) { return action.payload; } return selectedSong; }; export default combineReducers({ songs: songsReducer, selectedSong: selectedSongReducer, });
SongDetails.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React from 'react' ;import { connect } from 'react-redux' ;const SongDetail = ({ song } ) => { if (!song) { return <div > Select a song</div > ; } return ( <div> <h3>Details for :</h3> <p> Title: {song.title} <br /> Duration: {song.duration} </p> </div> ); }; const mapStateToProps = state => { return { song: state.selectedSong, }; }; export default connect(mapStateToProps)(SongDetail);
an other example may be more clear:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <script type="text/babel" data-plugins="proposal-class-properties" data-presets="env,react" > const increment = () => ({ type : 'increment' }); const decrement = () => ({ type : 'decrement' }); const Counter = (props ) => { return ( <div> <button className="increment" onClick={props.increment}>Increment</button> <button className="decrement" onClick={props.decrement}>Decrement</button> Current Count: <span > {props.count}</span > </div> ); }; const mapStateToProps = (state ) => { return { count: state.count } } const WrappedCounter = ReactRedux.connect(mapStateToProps, {increment, decrement})(Counter); const store = Redux.createStore(Redux.combineReducers({ count: (count = 0 , action ) => { if (action.type === 'increment' ) { return count + 1 ; } else if (action.type === 'decrement' ) { return count - 1 ; } else { return count; } } })); ReactDOM.render( <ReactRedux.Provider store={store}> <WrappedCounter /> </ReactRedux.Provider>, document .querySelector('#root' ) ); </script> <!--The App component above will be rendered into this --> <div id="root" ></div> <!--No need to change anything after this line!--> <!--No need to change anything after this line!--> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.0.0/polyfill.min.js" ></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js" ></script> <script src="https://unpkg.com/@babel/preset-env-standalone@7/babel-preset-env.min.js" ></script> <script src="https://unpkg.com/react@16/umd/react.production.min.js" ></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" ></script> <script src="https://unpkg.com/redux@4.0.1/dist/redux.js" ></script> <script src="https://unpkg.com/react-redux@5.0.6/dist/react-redux.js" ></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" />
An example of conditional render
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class _SongList extends React .Component { renderList ( ) { return this .props.songs.map(song => { return ( <div className="item" key={song.title}> <div className="right floated content" > <div className="ui button primary" >Select</div> </div> <div className="content" >{song.title}</div> <b>{song.title === this .props.favoriteTitle && 'Favorite!' }</b> </div> ); }); } render ( ) { return <div className ="ui divided list" > {this.renderList()}</div > ; } }
Section 18: Async Actions with Redux Thunk