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

Screen Shot 2020-10-24 at 00.01.59

Section 7: Handling User Input with Forms and Events

Event Handler

pPcPQgU

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();
//this "this"
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>
)
}

}

Screen Shot 2020-10-25 at 14.22.23

Let’s see some examples:

Screen Shot 2020-10-25 at 14.01.59

“this” refers to where the function is called

here, this === car object

Screen Shot 2020-10-25 at 14.14.22Here car.drive equals return this.sound

When we call truck.driveMyTruck(), “this” refers to truck object

Screen Shot 2020-10-25 at 14.18.08

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

Screen Shot 2020-10-25 at 14.40.49

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" }}>
//attention here
<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();
//use props(the function passed from parent component) here
//pass variable in child component as parameters of parent component function
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

Screen Shot 2020-10-25 at 15.38.01

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

Screen Shot 2020-10-25 at 17.19.07

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

Screen Shot 2020-10-25 at 18.00.07

Screen Shot 2020-10-25 at 18.15.00

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

Screen Shot 2020-10-25 at 20.58.35

Screen Shot 2020-10-25 at 21.59.16

In a reducer, we always return a new data instead of modifying the previous one.

Screen Shot 2020-10-25 at 21.45.34

remove an element from list with filter

Screen Shot 2020-10-25 at 21.49.40

Screen Shot 2020-10-25 at 21.59.16

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();

// People dropping off a form (Action Creators)
const createPolicy = (name, amount) => {
return { // Action (a form in our analogy)
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
}
};
};


// Reducers (Departments!)
const claimsHistory = (oldListOfClaims = [], action) => {
if (action.type === 'CREATE_CLAIM') {
// we care about this action (FORM!)
return [...oldListOfClaims, action.payload];
}

// we don't care the action (form!!)
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

Screen Shot 2020-10-25 at 22.59.31

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

Screen Shot 2020-10-26 at 23.33.23

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:

Screen Shot 2020-11-10 at 00.16.30

actions-index.js

1
2
3
4
5
6
7
//action creator
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"
// create an action when clicked
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>;
}
}

//listen to any changes to the global state in store
const mapStateToProps = state => {
console.log(state);
return { songs: state.songs };
};

//pass action to connect() automatically dispatch it to reducer
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' },
];
};

//reducer handle the action passed to it, change the global state in combineReducers
const selectedSongReducer = (selectedSong = null, action) => {
if (action.type === 'SONG_SELECTED') {
return action.payload;
}

return selectedSong;
};

//keys here is the key in global state
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';

//here {song} is the destructure of this.props
const SongDetail = ({ song }) => {
//song is null if there is no song selected
if (!song) {
return <div>Select a song</div>;
}
return (
<div>
<h3>Details for :</h3>
<p>
Title: {song.title}
<br />
Duration: {song.duration}
</p>
</div>
);
};

//receive global state changes(a song selected) and pass it to my props
const mapStateToProps = state => {
return {
song: state.selectedSong,
};
};
export default connect(mapStateToProps)(SongDetail);

an other example may be more clear:

Screen Shot 2020-11-10 at 00.21.07

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">
// Action Creators - You don't need to change these
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);

// Only change code *before* me!
// -----------

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

Screen Shot 2020-11-09 at 18.54.54

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>
//if song is favorite
<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