### Redux

Spring 2019 。 Ric Huang
---
### Recall: React props and state
* "this.props" are read-only
* You cannot assign or change values to this.props
* “state” is private to the class
* You cannot pass in value to it
* React 的出現,已經讓前端的可預測性大大提高
----
#### 但當 React Web App 複雜到一定程度的時候...
* Components 的關係錯綜複雜,states 的邏輯也隱晦在不同的 components 當中,越來越難理解與維護

----
### States 的愛恨情仇 ([ref](https://www.valentinog.com/blog/redux/))
* Multiple React components needs to access the same state but do not have any parent/child relationship
* You start to feel awkward passing down the state to multiple components with props
----
### 能不能集中管理 State ?
---
### Flux: Single directional data flow

----
### 集中管理的 state

----
### Unidirectional data flow

----
### Flux 因為一些歷史因素和本身的缺點,被另一套繼承自它且更簡單的框架取代 (Redux)
---
### Redux
React EU Conference 意外做出來的

<small>by Dan Abramov</small>
----
### Redux 三大原則
1. **Single source of Truth**
* Store: 整個前端 App 的 state 全存在唯一的樹狀 store 裡面
2. **The only way to change the state is by sending a signal to the store**
* Action: 改變 store 的唯一方式是送一個描述改變的 object (i.e. dispatching an action)
3. **The state is immutable and cannot change in place**
* Reducer: store 根據 action 決定 state 如何變化,但必須寫成一個 Pure Function
----

----
### A quick glance on Redux
* Store: a single place to store all the states for the app
* Action: a object that contains two properties: type and payload, to describes what updates the states will be
* Reducer: a function that takes the current state and an action as inputs and determines the next state
---
### A simple practice on Redux ([ref](https://www.valentinog.com/blog/redux/))
* First let's create a new React project and change *src/App.js* to the following:
```jsx
import React, { Component } from "react";
class App extends Component {
constructor() {
super();
this.state = {
articles: [
{ title: "React Redux Tutorial for Beginners", id: 1 },
{ title: "Redux e React: cos'è Redux e come usarlo con React", id: 2 }
]
};
}
render() {
const { articles } = this.state;
return <ul>{articles.map(el => <li key={el.id}>{el.title}</li>)}</ul>;
}
}
export default App;
```
* *npm start* to check what you see!
----
### Let's install **Redux** to dev
```bash
npm i redux --save-dev
```
----
### Create the "Store"
// src/js/store/index.js
```javascript
import { createStore } from "redux";
import rootReducer from "../reducers/index";
const store = createStore(rootReducer);
export default store;
```
----
### Define the "Action"
// Define constants first
// src/js/constants/action-types.js
```javascript
export const ADD_ARTICLE = "ADD_ARTICLE";
```
// src/js/actions/index.js
```javascript
import { ADD_ARTICLE } from "../constants/action-types";
export function addArticle(payload) {
return { type: ADD_ARTICLE, payload };
}
```
----
### Define the "Reducer"
// src/js/reducers/index.js
```javascript
import { ADD_ARTICLE } from "../constants/action-types";
const initialState = {
articles: []
};
function rootReducer(state = initialState, action) {
if (action.type === ADD_ARTICLE) {
state.articles.push(action.payload);
}
return state;
}
export default rootReducer;
```
----
### However, you should make the reducer "pure"
* Change this line:
```javascript
state.articles.push(action.payload);
```
to --
```javascript
return Object.assign({}, state, {
articles: state.articles.concat(action.payload)
});
```
----
### Let's stop and see what we have now...
* We have a React app that defines an App with a init state of two articles in an array
```bash
src/index.js
src/App.js
```
* We have defined a store, reducer, and an action in ---
```bash
src/js/store/index.js
src/js/reducers/index.js
src/js/actions/index.js
```
=> However, the Redux store and React App are not connected...
---
### How do we connect Redux state and React App?
* First let's see how store is operated
* To test it, add *src/js/index.js" to expose the store to a **window** property so that we can test it on console
```javascript
import store from "../js/store/index";
import { addArticle } from "../js/actions/index";
window.store = store;
window.addArticle = addArticle;
```
* Modify *src/index.js* as:
```javascript
import index from "./js/index"
```
----
### Testing "store"
* *npm start* and open console for "localhost:3000"
* Test the following:
```javascript
// Check the state now
store.getState()
// The subscribe method accepts a callback that will fire whenever an action is dispatched.
store.subscribe(() => console.log('Look ma, Redux!!'))
// Manually dispatch an action
store.dispatch( addArticle({ title: 'React Redux Tutorial for Beginners', id: 1 }) )
// Check the state again!
store.getState()
```
----
### 所以結論是...
* 我們可以透過 console (i.e. expose *store* to *window*) 來直接操作 store 的 methods,更改 state 裡頭的資料
* Yes, Redux is framework agnostic. 你可以把 Redux 用在任何的 framework, 除了 React, 也可以用在 Angular, Venilla, etc...
* 那到底要怎麼把 Redux 用在 React 呢?
* 重點是:要如何讓 React 的 states 被 connected 到 Redux 的 store 呢?
----
### 像前幾頁的這張圖

---
### React-Redux
* For React to use Redux, you should install react-redux
```bash
npm i react-redux --save-dev
```
----
### Provider: the wrapper
* We will first use *Provider*, an high order component coming from react-redux which wraps up your React application and makes it aware of the entire Redux’s store.
----
* Modify "src/js/index.js" as ---
```javascript
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import store from "./store/index";
import App from "./components/App.jsx";
// if you're in create-react-app import the files as:
// import store from "./js/store/index";
// import App from "./js/components/App.jsx";
render(
<Provider store={store}>
<App />
</Provider>,
// The target element might be either root or app,
// depending on your development environment
// document.getElementById("app")
document.getElementById("root")
);
```
----
### Create the List of articles
// src/js/components/List.jsx
```javascript
import React from "react";
import { connect } from "react-redux";
const mapStateToProps = state => {
return { articles: state.articles };
};
const ConnectedList = ({ articles }) => (
<ul className="list-group list-group-flush">
{articles.map(el => (
<li className="list-group-item" key={el.id}>
{el.title}
</li>
))}
</ul>
);
const List = connect(mapStateToProps)(ConnectedList);
export default List;
```
----
### Create Form.jsx
// src/js/components/Form.jsx
```jsx
import React, { Component } from "react";
import { connect } from "react-redux";
import uuidv1 from "uuid";
import { addArticle } from "../actions/index";
function mapDispatchToProps(dispatch) {
return {
addArticle: article => dispatch(addArticle(article))
};
}
class ConnectedForm extends Component {
constructor() {
super();
this.state = {
title: ""
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({ [event.target.id]: event.target.value });
}
handleSubmit(event) {
event.preventDefault();
const { title } = this.state;
const id = uuidv1();
this.props.addArticle({ title, id });
this.setState({ title: "" });
}
render() {
const { title } = this.state;
return (
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label htmlFor="title">Title</label>
<input
type="text"
className="form-control"
id="title"
value={title}
onChange={this.handleChange}
/>
</div>
<button type="submit" className="btn btn-success btn-lg">
SAVE
</button>
</form>
);
}
}
const Form = connect(null, mapDispatchToProps)(ConnectedForm);
export default Form;
```
----
### Create App.jsx
// src/js/components/App.jsx
```jsx
import React from "react";
import List from "./List.jsx";
import Form from "./Form.jsx";
const App = () => (
<div className="row mt-5">
<div className="col-md-4 offset-md-1">
<h2>Articles</h2>
<List />
</div>
<div className="col-md-4 offset-md-1">
<h2>Add a new article</h2>
<Form />
</div>
</div>
);
export default App;
```
----
### Lastly, add the Bootstrap style
// Add this line to public/index.html
```htmlmixed
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" >
```
----
### **npm start** to test it!!
---
### More to cover next time...
* Redux Middleware
* Asynchronous actions in Redux
* GraphQL: a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data
---
### Appendex: React and Express Example
```bash
mkdir react-express-test1 && cd react-express-test1
npm init -y
npm add express
```
// Edit a basic express index.js
```javascript
const express = require('express');
const path = require('path');
const app = express();
// Serve the static files from the React app
app.use(express.static(path.join(__dirname, 'client/build')));
// An api endpoint that returns a short list of items
app.get('/api/getList', (req,res) => {
var list = ["item1", "item2", "item3"];
res.json(list);
console.log('Sent list of items');
});
// Handles any requests that don't match the ones above
app.get('*', (req,res) =>{
res.sendFile(path.join(__dirname+'/client/build/index.html'));
});
const port = process.env.PORT || 5000;
app.listen(port);
console.log('App is listening on port ' + port);
```
----
In package.json
```bash
"scripts": {
"start": "nodemon index.js"
}
npm start
Open https://localhost:5000/api/getList
```
----
```bash
create-react-app client
cd client
npm start
open https://localhost:3000
```
In client/package.json, add ---
```bash
"proxy": "http://localhost:5000"
```
----
Edit App/App.js
```javascript
import React, { Component } from 'react';
import { Route, Switch } from 'react-router-dom';
import './App.css';
import Home from './pages/Home';
import List from './pages/List';
class App extends Component {
render() {
const App = () => (
<div>
<Switch>
<Route exact path='/' component={Home}/>
<Route path='/list' component={List}/>
</Switch>
</div>
)
return (
<Switch>
<App/>
</Switch>
);
}
}
export default App;
```
----
Edit App/pages/Home.js
```javascript
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
class Home extends Component {
render() {
return (
<div className="App">
<h1>Project Home</h1>
{/* Link to List.js */}
<Link to={'./list'}>
<button variant="raised">
My List
</button>
</Link>
</div>
);
}
}
export default Home;
```
----
Edit App/pages/List.js
```javascript
import React, { Component } from 'react';
class List extends Component {
// Initialize the state
constructor(props){
super(props);
this.state = {
list: []
}
}
// Fetch the list on first mount
componentDidMount() {
this.getList();
}
// Retrieves the list of items from the Express app
getList = () => {
fetch('/api/getList')
.then(res => res.json())
.then(list => this.setState({ list }))
}
render() {
const { list } = this.state;
return (
<div className="App">
<h1>List of Items</h1>
{/* Check to see if any items are found*/}
{list.length ? (
<div>
{/* Render the list of items */}
{list.map((item) => {
return(
<div>
{item}
</div>
);
})}
</div>
) : (
<div>
<h2>No List Items Found</h2>
</div>
)
}
</div>
);
}
}
export default List;
```
----
```bash
# In react-express-test1
npm start
# In react-expresss-test1/client
npm start
open https://localhost:3000
```
---
{"metaMigratedAt":"2023-06-14T21:51:15.526Z","metaMigratedFrom":"YAML","title":"Redux","breaks":true,"slideOptions":"{\"theme\":\"beige\",\"transition\":\"fade\",\"slidenumber\":true}","contributors":"[{\"id\":\"752a44cb-2596-4186-8de2-038ab32eec6b\",\"add\":14048,\"del\":1242}]"}