### Redux ![](https://i.imgur.com/HV34nOU.png) 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 當中,越來越難理解與維護 ![](https://i.imgur.com/9xPq9xw.png) ---- ### 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 ![](https://i.imgur.com/57z1lr3.png) ---- ### 集中管理的 state ![](https://i.imgur.com/PQgIAm6.png) ---- ### Unidirectional data flow ![](https://i.imgur.com/BHe7wJV.png) ---- ### Flux 因為一些歷史因素和本身的缺點,被另一套繼承自它且更簡單的框架取代 (Redux) --- ### Redux React EU Conference 意外做出來的 ![](https://i.imgur.com/lRmPEBg.png) <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 ---- ![](https://i.imgur.com/ooLQ94v.png) ---- ### 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 呢? ---- ### 像前幾頁的這張圖 ![](https://i.imgur.com/PQgIAm6.png) --- ### 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}]"}
    1268 views