#### Advanced Topics on React.js and React Router ![](https://i.imgur.com/3cGV1KY.png) Spring 2019 。 Ric Huang --- ### Example in previous meeting... ![](https://i.imgur.com/7xQrY3r.png) ---- ### In "index.js" ```jsx import Counter from './containers/Counter'; ReactDOM.render(<Counter / >, document.getElementById('root')); ``` ---- ### In "Containers/Counter.js" ```jsx import Button from "../components/Button"; import Input from '../components/Input'; class Counter extends Component { constructor(props) { super(props); this.state = { count: 100 }; } ... some methods for class Counter render() { return ( <div> <h1>{this.state.count}</h1> <span> <Button text="+" onClick={this.handleInc} /> <Button text="-" onClick={this.handleDec} /> <Input onKeyPress={this.handleInput} /> </span> </div> ); } } ``` ---- ```jsx // In Components/Button.js import React from 'react' export default ({ onClick, text }) => { return <button onClick={onClick}>{text}</button>; } // In Components/Input.js import React from 'react'; export default ({onKeyPress}) => { return <input type="text" placeholder="Enter a number..." onKeyPress={onKeyPress} />; } ``` ---- ```jsx class Counter extends Component { ... handleInc = () => this.setState(state => ({ count: state.count + 1 })); handleDec = () => this.setState(state => ({ count: state.count - 1 })); setNumber = num => this.setState(() => ({ count: num })); handleInput = e => { if (e.key === "Enter") { const value = parseInt(e.target.value); if (value === 0 || value) this.setNumber(value); e.target.value = ""; e.target.blur(); } }; ... } ``` --- ### Advanced Topics on React.js 1. 善用 Array and arrow functions ([ref](https://reactjs.org/docs/lists-and-keys.html)) 2. Composition and props.children ([ref](https://reactjs.org/docs/composition-vs-inheritance.html)) 3. React.Fragment ([ref](https://reactjs.org/docs/fragments.html)) 4. Higher Order Component (HOC) ([ref](https://reactjs.org/docs/higher-order-components.html)) 5. React Refs ([ref](https://reactjs.org/docs/refs-and-the-dom.html)) 6. Forwarding refs ([ref](https://reactjs.org/docs/forwarding-refs.html)) --- ### 善用 Array and arrow functions ---- ### [範例] TodoList.js ```jsx import classes from './TodoList.module.css' import TodoItem from './TodoItem/TodoItem' export default ({ items, toggleItem, removeItem, save }) => { if (!items.length) return null; return ( <ul className={classes.List}> {items.map((ele, key) => ( <TodoItem item={ele} key={key} identity={key} toggleItem={toggleItem} removeItem={removeItem} save={save} / > ))} </ul> ); } ``` ---- * 在前頁的例子 ```jsx return ( <ul>{an array of React Components}</ul> ) ``` * 而 array of React Components 則是用 --- ```jsx Array.map( element => (some JSX expression) ); ``` 來實現 ---- #### "key" for React array of components * 請注意:身為一個新手,當你產生一個 array of components 的時候,你很可能會得到 warning 說:*Warning: Each child in a list should have a unique "key" prop.* * 這是因為對於 listed items, React 是用內建的 global attribute "key" 來決定哪些 items 有被修改或是增減,所以如果你沒有 specify "key", 沒有加上 "key" attribute, 你的 React Component 的行為可能會跟你的預期不同 * Note: 不過要小心 "key" 的 value 要維持 unique, 不可以有不同的 listed items 有相同的 keys。更多 details, 可以看看 React 的 VDOM 如何察覺變化 ([ref](https://reactjs.org/docs/reconciliation.html)) --- ### Compositional Model * 想像一下,在某個 blog site, 隨著瀏覽頁面不同,你看到的內容也會不一樣。不過,這些頁面似乎都是由一些基本元件所組成 --- memnu bar, article, images, comments, etc. 這種由一些 components 來組成頁面的設計方法,是 React 裡面很重要也很基本的 "Compositional Model" ---- ### props.children * 不過,在 compositional model 裏頭的 child components 並不是 listed items, 並且在宣告的時候,並沒有確定有哪些 children, 而是在應用時才被 instantiated. 例如: ```html <h1> This is a title </h1> <p> This is a paragraph </p> <div> This is a div </div> <footer> This is a footer </footer> ``` ---- * 這時候你可以用內建的 **"props.children"** 來宣告一個 "黑盒子",讓 instantiate 這個 component 的物件來決定 children 由哪些 components 來組成: ```jsx class BlogPage extends Component { render () { return <div className="blog-page"> {this.props.children} </div>; } } ``` ---- // In "App.js" ```jsx class App extends Component { render() { const contents = []; contents.push(<h1> This is a title </h1>); contents.push(<p> This is a paragraph </p>); contents.push(<div> This is a div </div>); contents.push(<footer> This is a footer </footer>); return <BlogPage children={contents} />; } } ``` --- ### React.Fragment * Recall: 在 React Component 的 render() 裏頭,你必須 return 一個 single root node. 但當我們需要 return multiple nodes 的時候,一個解法是用 "\<div>\</div>" 包起來。但是,如果 caller 的 children 不可以是 "\<div>" 怎麼辦呢? ```jsx class MyTable extends Component { render() { return <table> <MyData dataInput={data1}/ > <MyData dataInput={data2}/ > </table> } } ``` ---- ### Oops, \<tr> is expected... ```jsx class MyData extends Component { render() { return ( <div> <tr>{some data}</tr> <tr>{some data}</tr> </div> ); } } ``` ---- ### Use React.Fragment to solve it! ```jsx import React, { Fragment } from 'react'; class MyData extends Component { render() { return ( <Fragment> <tr>{some data}</tr> <tr>{some data}</tr> </Fragment> ); } } ``` ---- * It can also be written as... ```jsx import React, { Fragment } from 'react'; class MyData extends Component { render() { return ( <> <tr>{some data}</tr> <tr>{some data}</tr> </> ); } } ``` * However, some browsers may not yet support this short form yet. Use it with care! * Note: "key" is the only attribute that can be used in \<Fragment>. Event handlers are not supported yet. --- ### Higher Order Component (HOC) * **"Higher Order Component"** 是另外一個 React 裡頭常用的 programming technique --- 想像你的 nevigation bar 會隨著 logged in user 不同而有不同的內容/layout,或者是你的 blog page 會隨著文章種類的不同而選擇不同的來源... 等,但他們的 event binding, error handling, 或者是其他的邏輯是一樣的,所以你會想要有一個 **"產生 component 的 function"**, 可以吃進一個 component 當參數,然後也許吃進另一個 callback 當作客製化 layout/data source 的方法,像這樣 (next page): ---- ```jsx const generalNavBar = (WrappedNavBar, layoutMethod) => return class extends Component { constructor(props) {...} ... some life-cycle methods or event handling logic render() { return <WrappedNavBar ... / >; } } ``` * Note: 1. 基本上, HOC 就是用一個 function, 吃進一個 wrapped component, 產生另一個 higher-order component 2. 參數列不限個數,但第一個 arg 通常是 wrapped component 3. "return class extends" <== anonymous class ---- ### Another HOC example ```jsx const withAuthGuard = WrappedComponent => { return class extends Component { render() { return ( <Query query={ME_QUERY} onError={error => {...; }}}> { ({ data, loading, error }) => { if (loading) return <WrappedComponent loading={true} /> if (error) return <WrappedComponent isAuth={false} /> return ( <WrappedComponent isAuth={true} username={data.me.username} />)} } </Query>)} } } ``` ---- ### HOC should be pure! * Note that a HOC doesn’t modify the input component, nor does it use inheritance to copy its behavior. Rather, a HOC composes the original component by wrapping it in a container component. A HOC is a pure function with zero side-effects. * However, don't apply HOC in render(). This will cause the subtree to unmount/remount each time! ([ref](https://reactjs.org/docs/higher-order-components.html#dont-use-hocs-inside-the-render-method)) --- ### React Refs * 當你要從介面去改動一個很深的 component 的時候,正常來說你要透過一層層 props 的傳遞,才能把要改的資料/執行的動作傳到底層的元件。但,有時候這樣寫不是很直觀、或者是當牽涉到你不應該去改動的第三方 modules 的時候,一個有點暴力、雖然不建議但偶爾可以用一下的方法就是 create 一個 React ref, 就像是一個 endpoint, 讓外部的使用者可以直接 access 到這個 DOM node/component * 很明顯的,這樣做會破壞 encapsulation, 所以在加入 React Ref 之前,應該要思考一下是否真的有必要這麼做? (我們未來會再教一些比較好、安全的做法) ---- ### React Refs Example ```jsx class App extends Component { constructor(props) { super(props); this.textInput = React.createRef(); } focusTextInput = () => { this.textInput.current.focus(); }; render() { return ( <div> <textarea rows="4" cols="50" ref={this.textInput} / > <input type="button" value="Click to edit" onClick={this.focusTextInput} /> </div> ); } } ``` ---- * In the previous example --- ```jsx class App extends Component { constructor(props) { super(props); // 產生一個 React ref, 先把它存在 this.textInput 裡 this.textInput = React.createRef(); } ... render() { return ( <div> // 產生一個 <textarea> element, 並且把存在 // this.textInput 的 reference 指到這個 element <textarea ... ref={this.textInput} / > ... </div> ); } } ``` ---- * Previous example (continued) ```jsx class App extends Component { ... render() { return ( <div> ... // 當這個 input button 被 click 的時候, // 呼叫 this.focusTextInput <input type="button" value="Click to edit" onClick={this.focusTextInput} /> </div> ); } } ``` ---- * Previous example (continued) ```jsx class App extends Component { ... // 當 focusTextInput 被呼叫的時候,用 this.textInput.current // 抓到目前被 referred 的 element, 也就是下面的 <textArea>, // 並且執行它的 .focus() focusTextInput = () => { this.textInput.current.focus(); }; render() { return ( <div> <textarea rows="4" cols="50" ref={this.textInput} / > ... </div> ); } } ``` ---- * 如果有兩個 elements 都設定 refs... 請分別 createRef() ```jsx class App extends Component { constructor(props) { super(props); this.textInput1 = React.createRef(); this.textInput2 = React.createRef(); } ... render() { return ( <div> <textarea ... ref={this.textInput1} / > <textarea ... ref={this.textInput2} / > ... </div> ); } } ``` --- ### Forwarding Refs * 有時候必須將 local 拿到 child or HOC 的 component 來作為 ref, 以利在 local 做一些直接的操作 * 語法: ```jsx const SomeComponent = React.forwardRef( (props, ref) => (<ChildComponent ref={ref} ... / >) ); class App extends Component { ... this.ref = React.createRef(); // this.ref.current will become <ChildComponent> render() { return ( <SomeComponent ref={this.ref}... / > ); } } ``` ---- ### 完整範例 ```jsx const MyDiv = React.forwardRef((props, ref) => ( <div ref={ref} className="MyDiv">{props.children}</div> )); class App extends Component { constructor(props) { super(props); this.ref = React.createRef(); } componentDidMount() { console.log(this.ref.current); } render() { return ( <MyDiv ref={this.ref}> ... </MyDiv> )} } ``` --- 學習完一些 advanced React topics 之後, 我們接下來要來學 **React 生態系** 裡頭一個很基礎, 也很重要的部分:**React Router** --- ### React Router ---- ### Motivations and Backgrounds: Server-Side (後端) vs Client-Side (前端) Rendering * Recall: 我們這邊講的**前端**是指你所使用的瀏覽器,負責收到 HTML & data 以後顯示出網頁,而**後端**是指 Web Server, 在收到 http request 之後一方面視需求到資料庫存取、修改資料,然後把 HTML & data 回傳給前端顯示 ---- ### Server-Side (後端) vs Client-Side (前端) Rendering * **"Render"** 一般翻譯成 **"渲染"** 在網頁上就是把頁面畫出來的意思。當使用者點選連結、切換頁面的時候,到底是後端把整 HTML 處理好之後,再傳給前端畫出來 (i.e. server-side rendering),還是後端只把必要資料處理好之後傳給前端,再由前端處理產生 HTML 再畫出來 (i.e. client-side rendering) 呢? ---- ### Server-Side (後端) Rendering * 如果使用者開啟一個新的網址,server-side rendering 會讓前端拿到一個新的 HTML,他會看到畫面刷新,如果網路 lag 或是網頁寫得不夠好的話,甚至會看到「白畫面」 * 想想如果是在聽音樂、玩遊戲,這樣的體驗當然很不 OK! ---- ### Server-Side Rendering ([圖示](https://blog.techbridge.cc/2017/09/16/frontend-backend-mvc/)) ![](https://i.imgur.com/3ZMglpy.png) ---- ### Client-Side (前端) Rendering * Clinet-side rendering 就是利用一些像是 VDOM 的技術,讓使用者點選一些連結的時候,前端網頁只是透過 API 像後端要資料,而前端拿回資料後再更新 DOM 需要更新的 HTML, 動態的更新那部份的頁面。 * 這樣的做法通常會讓前端的 code 變得複雜許多,但還好現在許多前端技術 (e.g. React Routing, GraphQL) 讓這一切寫起來比較乾淨、也比較模組化 ---- ### Client-Side Rendering ([圖示](https://blog.techbridge.cc/2017/09/16/frontend-backend-mvc/)) ![](https://i.imgur.com/2X2JcnB.png) ---- ### SPA (Single Page Application) * 不過前述的 client-side rendering 常常配合著所謂的 SPA (Single Page Application) 的實現方法,也就是說,前端事實上只有一個 index.html, 所以使用者在切換連結的時候只是發出 Ajax/JSON API request, 從後端拿資料,然後前端並沒有切換頁面,所以可以做到像是使用者一邊在網頁上看影片,一方面點選頁面上的連結去查看作者、影片相關資訊,而不會影響到影片的播放。 ---- ### Client-Side Routing * 不過再想像一個情況,假設你在瀏覽一個部落格或是論壇,從一篇文章切換到另外一篇文章,由於 client-side rendering 的關係,所以頁面上者有文章更新的部分被 update, 所以看起來很順。 * 但問題是,當你很直覺的按瀏覽器的「上一頁」,想要回到上一篇文章的時候,你會發現沒有用!因為你從頭到尾都是在 "index.html" 這頁上面啊! ---- ### Client-Side Routing * Client-Side Routing 讓你在 local 端產生瀏覽器的 routing, 像是: * ...myblog.com/home * ...myblog.com/posts * ...myblog.com/posts/13 * ...myblog.com/users/ric * 在瀏覽到不同頁面的時候會有對應到不同的 routing (web address),而被存到瀏覽器的 history 中,可以使用前/後一頁 ---- #### Client-Side Rendering/Routing ([圖示](https://blog.techbridge.cc/2017/09/16/frontend-backend-mvc/)) ![](https://i.imgur.com/tjYRTSz.png) --- ### React Router * React Router 是整個 「React 生態系」的一部分,通過管理 URL,實現頁面以及狀態切換可以「模組化」,讓 code 更好管理,也比較容易理解,也能符合 React 基本只更新 minimum difference 的概念 ---- ### 安裝與使用 React Router * 安裝:npm install react-router-dom * 如果遇到建議要 "npm audit fix",就 fix 吧! * 使用: ```jsx import { BrowserRouter } from 'react-router-dom' import { NavLink, Switch, Route } from 'react-router-dom' ``` ---- ### 一個簡單的應用情境 * 假設你寫了一個 blog page, 你規劃了: * '/' or '/home':主畫面 * '/posts':顯示所有文章列表 * '/posts/\<postId>':顯示某篇文章 * '/authors':顯示所有作者列表 * '/authors/\<authorName>':顯示某位作者的文章列表 ---- ### Top-level "App.js" ```jsx class App extends Component { render() { return ( <BrowserRouter> <div className="App"> <Blog / > </div> </BrowserRouter> ) } } ``` * 定義了這個 APP 的 root directory (i.e. '/') ---- ### In "Blog.js" ```jsx class Blog extends Component { render() { return ( <div> // Define your blog layout ... <NavLink to="/home">Home</NavLink> <NavLink to="/posts">Posts</NavLink> <NavLink to="/authors">Authors</NavLink> ... <Switch> <Route exact path="/posts" component={Posts} / > <Route path="/posts/:id?" component={PostRoute} / > <Route exact path="/authors" components={Authors} / > <Route path="/authors/:name?" components={AuthorRoute} / > <Redirect from="/Home" to="/" / > </Switch> </div> ) } } ``` ---- #### \<NavLink to="/home">Home\</NavLink> * 定義 "Home" 這個字所對應的 routing path * 其中,'/' 代表這個 App 的根目錄 * **\<NavLink>** 只是用來代表一個 link 而已,真正在頁面上畫出來,還是要在外面包一個 HTML tag, 例如: * \<p>\<NavLink to="/home">Home\</NavLink>\</p> * \<li>\<NavLink to="/home">Home\</NavLink>\</li> * \<button>\<NavLink to="/home">Home\</NavLink>\</button> * 換句話說,在畫面點下 "Home" 的時候,頁面會 route 到 ""...AppHome/home" ---- ### \<NavLink> vs. \<Link>? * 有時候你會在別的範例看到別人使用 **\<Link>**, 而非 **\<NavLink>**, what's the difference? * 官方說明:*A special version of the \<Link> that will add styling attributes to the rendered element when it matches the current URL.* ---- ### \<Switch>...\</Switch> * 用來定義這個 App 的所有 routings 如何產生畫面 * 一個 **\<Switch>** 裡面包著多個 **\<Route / >**,而每個 **\<Route / >** 用來指定在 \<NavLink> 所定義的 path 連結, 要用哪一個 React compoment 來產生畫面呢? * 基本 \<Route> 的語法 ```jsx <Route path="/someDir" component={SomeComponent} / > ``` ---- ### Putting things together... ```jsx class Blog extends Component { render() { return ( <div> // Define your blog layout <ul> <li><NavLink to="/posts">Posts</NavLink></li> <li><NavLink to="/authors">Authors</NavLink></li> </ul> <Switch> <Route path="/posts" component={Posts} / > <Route path="/authors" components={Authors} / > </Switch> </div> ) } } ``` ---- ### URL Parameters * 通常會把文章根據 IDs, 或者是作者根據名字,來安排至不同的 routings, 例如: * .../posts/12345678 * .../authors/ric * 但隨著 Blog 的文章會增加、讀者/作者數量也會增加,不可能在 Blog.js 裡頭把這些文章、作者頁面的 routings 全部預先寫死。因此,我們要用 "參數" 來指定 routing 的規則。例如: ```jsx <Route path="/posts/:id?" component={PostRender} / > ``` ---- ### \<Route path="/posts/:id?" component={PostRender} / > * 當你定義這行時,你事實上就定義了指定的 component (i.e. PostRender) 的 **props.match.params** 多了一個 **"id"** 這個 property * 換句話說,當你連結 ".../posts/3838" 的時候,就等於把 3838 當作參數傳給 **PostRender** 的 **props.match.params.id**, 你可以在 PostRender 裡頭根據 id 去處理拿到文章的邏輯。 ---- ### "exact path" for \<Route> 不過當這兩行同時存在的時候... ```jsx // 列舉所有文章 <Route path="/posts" component={Posts} / > // 展示某篇文章 <Route path="/posts/:id?" component={PostRender} / > ``` 當網址是 "**.../posts/3838**" 的時候,事實上兩條 routing rules 都會符合,所以照順序,會吐出第一個 match (列舉所有文章),而不是展示 3838 這篇文章 所以,第一條應該要改成 **exact path**: ```jsx <Route exact path="/posts" component={Posts} / > ``` ---- ### URL Redirect * 用途:將某個 path ".../pathA" redirect 到另一個 path ".../pathB" ```jsx <Redirect from="pathA" to="pathB" / > ``` --- ### Let's look at a simple yet complete example! Download ["react-router-boilerplate.tgz"](https://github.com/ric2k1/ric2k1.github.io/blob/master/W8_0410/react-router-boilerplate.tgz) from Github! ---- ### Install and Run! * tar zxvf react-router-boilerplate.tgz * cd react-router-boilerplate * npm install * (If needed) npm audit fix [--force] * npm start ---- ### Code tree (under "src") ``` * index.js // to mount "root" DOM node * App.js // Define Routing root '/' * containers/ * Blog/ * Blog.js // Define main page and routing rules * Posts/ * Posts.js // List all posts (Posts module) * PostRender.js // Define how to generate posts * components/ * Post * Post.js // Define Post module ``` ---- ### Posts.js (列舉文章列表) * Note: 理論上文章等資料都應該是從後台 (backend server) 過來,但我們現在還沒有(教)後台,所以我們會把資料寫死在 JS 檔案裡 ```jsx export default class Posts extends Component { render() { const postIDs = ["1", "3", "5", "7", "9"]; const lists = postIDs.map((i, index) => ( <li key={index}> <NavLink to={"/posts/" + i}>Posts #{i}</NavLink> </li>)); return ( <div> <h3>Click to view article ---</h3> {lists} </div>); } } ``` ---- ### **\<li key={index}>** // See p3.3 * 如果把 "key={index}" 拿掉,你會看到這樣的 message: *Warning: Each child in a list should have a unique "key" prop.* * 原則上如果文章在後台管理,你可以 assign 每一篇文章一個 unique ID, 然後就可以利用這個 ID 來當 \<li> 的 unique keys * 所以我們在這邊先使用 Array 的 index 來當 key ---- ### \<NavLink to={"/posts/" + i}> ### Posts #{i}</NavLink> * 用來指定當點選某篇文章連結的時候,會 route 到當篇文章的網址 (defined in "Blog.js") ---- ### PostRender.js (定義如何產生 posts) ```jsx export default class PostRender extends Component { render() { const postIDs = ["1", "3", "5", "7", "9"]; const { id } = this.props.match.params; return id && postIDs.includes(id) ? ( <Post id={id} /> ) : ( <div> <h3>Error: Post #{id} NOT FOUND</h3> </div> ); } } ``` * Question: Posts 與 PostRender 可否合在一個 class? ---- * const { id } = this.props.match.params; => This is ["Destructuring Assignment"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), which is equivalent to: * const id = this.props.match.params.id; * You can also do something like: const { a: b } = obj.someProp, which is equivalent to: * const b = obj.someProp.a; --- ### Practice06:Create Routing for your blog page * Based on "react-router-boilerplate". Re-do your Practice03, "A Static Blog Page in React.js". Make it a **single-page application with React Routing**. * Since it is still serverless, hard-core the contents and list of articles, etc, in the files. * For those who want to have a fake database on front-end, you can try ["localStorage"](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). Or refer to this one: [react-router-blogs-example.tgz](https://github.com/ric2k1/ric2k1.github.io/blob/master/W8_0410/react-router-blogs-example.tgz) --- ## That's it! (You are recommended to go through this [official React Router Tutorial](https://reacttraining.com/react-router/web/guides/quick-start))
{"metaMigratedAt":"2023-06-14T20:58:38.577Z","metaMigratedFrom":"YAML","title":"Advanced Topics on React.js and React Router (04/10)","breaks":true,"slideOptions":"{\"theme\":\"beige\",\"transition\":\"fade\",\"slidenumber\":true}","contributors":"[{\"id\":\"752a44cb-2596-4186-8de2-038ab32eec6b\",\"add\":24251,\"del\":3667},{\"id\":\"3548892a-0c71-4f2e-92b5-26f7c7ea6e8e\",\"add\":20656,\"del\":20655},{\"id\":\"c20a8349-5a60-40ce-8b76-b98cc67ca5d6\",\"add\":0,\"del\":1},{\"id\":\"1616274c-795e-4491-b5fb-f94b3a9a2335\",\"add\":36,\"del\":0}]"}
    2227 views