React 學習筆記(1) - Hook, useState === ![](https://i.imgur.com/ZLirEbH.png) ###### tags: `React` --- ## 起步 ### 安裝 1. 終端機開啟後,到你想要存放專案的位置 2. 透過語法 `npx create-react-app my-app` 建立一個叫做 my-app 的專案資料夾 > npx 會確保下載到最新的版本,並且會立即執行;npm 會需要下載,安裝,再透過使用者呼叫才執行。 ![](https://i.imgur.com/OcpBhsn.png) 3. 詢問是否要繼續 `Ok to proceed? (y)`,點 y 即可進行相關安裝,安裝過程大概幾秒鐘的時間 ![](https://i.imgur.com/1aKaT7b.png) 4. 安裝完畢後,可以看到以下綠色的字代表你可以在終端機啟用或輸入的指令,以及 Happy hacking,即代表成功安裝。 ![](https://i.imgur.com/m4Ka41g.png) 5. 輸入`cd my-app` 進入資料夾,執行 `npm start` 開啟 localhost ![](https://i.imgur.com/EEnmIW2.png) 7. 開啟 http://192.168.1.107:3000 (視自己的 IP 而定) 若出現一個很大的 React Logo,即代表成功開啟 --- ### 介紹 #### package.json 1. --- ### 小試身手,開始撰寫一點 React 1. 試著透過這個 `<div id="root"></div>` 寫一點東西吧 ![](https://i.imgur.com/qG522AA.png) 2. 打開資料夾內的 index.js - [What is require in ReactJS](https://stackoverflow.com/questions/32623573/what-is-require-function-is-reactjs) ```javascript= /* 這裡的 require 我會理解成類似 import 的概念, 將 react 和 react-dom 模組分別存為兩個變數,可以參考 express */ const React = require("react"); const ReactDOM = require("react-dom"); /* 這裏建立一個 App 的函式,透過 React 模組來建立三個元素: - html tag h1: - css style : null (目前還未設定,所以給予空值) - h1 的內容:"This is coool" */ function App() { return React.createElement("h1", null, "This is coooool"); } /*透過 ReactDOM 模組渲染由 React 模組建立的元件 App, 並且將這個元件,放置到 <div id="root"></div> 裡面*/ ReactDOM.render(React.createElement(App), document.querySeletor("#root")); ``` 3. 顯示結果 ![](https://i.imgur.com/H1wfzQY.png) > 如果要建立多行該怎麼做呢?可以直接在 `funtciotn App()` 裡面 return 多個 html 標籤嗎,請參考下面? ```javascript= function App() { return React.createElement("h1", null, "This is coooool"); return React.createElement("h1", null, "This is coooool"); return React.createElement("h1", null, "This is coooool"); return React.createElement("h1", null, "This is coooool"); } ``` - [ ] - 可以 - [x] 不行 4. 如果要返回多個 html,要使用陣列 ```javascript= function App() { return React.createElement("div", null, [ React.createElement("h1", null, "This is my profile"), React.createElement("p", null, "Click to see my resume"), React.createElement("button", null, "Check") ]); } ReactDOM.render(React.createElement(App).document.querySelector("#root")); ``` 這裡的感覺很像是使用 Vanilla js 時,需要透過變數去建立 html element,在 appendchild。因此可以想成透過 React 模組先建立一個 `<div>` 的容器後依序建立三種子元素,再透過 ReactDOM 去渲染出來。 ![](https://i.imgur.com/0AqQvja.png) 5. 使用 ES6 修正上述的兩個模組的導入 ```javascript= import React from "react"; import ReactDOM from "react-dom"; ``` 6. 針對最後一行執行的程式做最佳化 ```javascript= ReactDOM.render(React.createElement(App).document.querySelector("#root")); /* 由於 function App() 基本上就是 return 裡面的值, 因此可以直接在 ReactDOM.render(App(),....) 直接執行即可。*/ ReactDOM.render(App().document.querySelector("#root")); ``` --- ### 使用 JSX 修改上述的程式 > 什麼是 JSX,簡單來說是 javaScript 的語法擴充,可以在寫 react 時,使用 html 標籤,但同時也保有 javaScript 的功能,可參考 [React 官網](https://zh-hant.reactjs.org/docs/introducing-jsx.html)。 ```javascript= // 原本的 code function App() { return React.createElement("div", null, [ React.createElement("h1", null, "This is my profile"), React.createElement("p", null, "Click to see my resume"), React.createElement("button", null, "Check") ]); } // 使用 JSX function App() { return ( <div> <h1>This is my profile</h1> <p>Click to see my resume</p> <button>Check</button> </div> ); } ``` #### 重點: 1. 定義一個 function,名稱要大寫,如果小寫是不行的 2. 使用 JSX 依舊只能回傳一個值,因此不能把 ```<div>``` 標籤拿掉 --- ### 透過 import 來把其他檔案導入進 main file Step 1: 在 src folder 裡面建立以下三個檔案 - index.js (main file for rendering) - App.js (component) - Nav.js (component) Step 2: 建立 Nav.js function ```javascript= // Nav.js 建立導覽列(均無 CSS 設定) import React from "react"; const Nav = () => { return ( <nav> <ul> <li><a href="#">Home</a></li> <li><a href="#">My resume</a></li> </ul> </nav> ); } // export export default Nav; ``` Step 3: 建立 App.js function ```javascript= // App.js import React from "react"; const App = () => { return ( <div> <h1>Hello world</h1> <p>This is my profile</p> <p>Contant me here</p> <div/> ); } export default App; ``` Step 4: Import App.js & Nav.js to index.js ```javascript= // index.js // import moduls import React from "react"; import ReactDOM from "react-dom"; // import App.js and Nav.js from local folder import App from "./App"; import Nav from "./Nav"; // Start rendering to html ReactDOM.render( <React.StricMode> <Nav /> <App /> </React.StricMode>, // Select which part you would like to put into the html file document.querySelector("#root") ); ![Uploading file..._najmown6o]() ``` ### Result ![](https://i.imgur.com/znnqciR.png) > 補充: > 1. [What is React.StricMode](https://zh-hant.reactjs.org/docs/strict-mode.html) > 2. Why do components in React need to be capitalized? 可以參考 [JSX In Depth](https://reactjs.org/docs/jsx-in-depth.html) > **lowercase names refer to built-in components, capitalized names refer to custom components** --- ### React app script ``` "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" ``` - react-scripts start : 開啟本地端伺服器 - react-scripts build : 打包程式碼,減少並最佳化即將要上線的 app 的記憶體,可減少網路負擔 - react-scripts test:測試 app,確認程式碼正常運作 - (**Do not EJECT**)react-scripts eject:可以把所有的設定值移出到根目錄之中,但是這是一個一次性的指令,如果你用了這個指令後,整個專案就回不去原先的專案的結構,之後的裡面的套件有新的版本要升級時,就只能靠你手動來升級每個裡面的套件。 參考:[iT邦幫忙:建置React開發環境](https://ithelp.ithome.com.tw/articles/10185954?sc=iThomeR) --- ## React 範例 ### Hooks vs. Classes - 建立 component 的時候,會有兩種方式: - Hooks 的建立主要是為了 React 本身 (also called functional component) - Class 基本上就是 JS,因此相容性問題幾乎沒有 (Called class component) ```javascript= // functional component aka react hook import "./App.css"; import { useState } from "react"; function App() { // useState return an array const [name, setName] = useState("Andrea"); const handleClick = () => { // setName here is the value that would be changed after clicking the btn setName("Kate"); console.log(name); }; return ( <div className="App"> <h1>My name is {name}</h1> <button onClick={handleClick}>Change Name</button> </div> ); } ``` ```javascript= //Class component import "./App.css"; import { Component } from "react"; class App extends Component { constructor() { super(); this.state = { name: "Andrea", }; } render() { return ( <div className="App"> <h1>My name is {this.state.name}</h1> <button>Change Name</button> </div> ); } } ``` --- ### outputing lists - 使用 useState 來建立一個陣列物件 ```javascript= import "./App.css"; import { useState } from "react"; function App() { // useState return an array const [name, setName] = useState("Andrea"); // outputing lists const [events, setEvents] = useState([ { title: "mario's birthday bash", id: 1 }, { title: "bowser's live stream", id: 2 }, { title: "race on moo moo farm", id: 3 }, ]); const handleClick = () => { setName("Kate"); console.log(name); }; return ( <div className="App"> <h1>My name is {name}</h1> <button onClick={handleClick}>Change Name</button> {events.map((event, index) => ( <div> <h2>{event.title}</h2> </div> ))} </div> ); } export default App; ``` 雖然成功輸出陣列內的每一個物件,但 console 卻顯示錯誤,原因在於 **Warning: Each child in a list should have a unique "key" prop.**,因此我們必須給予每一個清單項目獨特的 key prop 來讓 react 追蹤清單的刪減或增加 ![](https://i.imgur.com/yDM1Lta.png) ```javascript= // 在 div 加入 event.id {events.map((event) => ( <div key={event.id}> <h2>{event.title}</h2> </div> ))} </div> ``` 接著我們加入 id 來給每一個清單獨特的識別 ```javascript= // 加入 id {events.map((event, index) => ( <div key={event.id}> <h2>{index}{event.title}</h2> </div> ))} </div> // 或加入 index {events.map((event, index) => ( <div key={index}> <h2> {index}-{event.title} </h2> </div> ))} </div> ``` 有些習慣加入 index 作為 key prop,但 index 並不能夠真正地追蹤清單,原因是因為 index 會隨著刪減或新增而改變,這樣並沒有賦予每一個獨立項目特別的識別。 --- ### Using the previous state - 加入一個可以刪除的按鈕 ```javascript= import "./App.css"; import { useState } from "react"; function App() { // outputing lists const [events, setEvents] = useState([ { title: "mario's birthday bash", id: 1 }, { title: "bowser's live stream", id: 2 }, { title: "race on moo moo farm", id: 3 }, ]); const handleClick = (id) => { setEvents( events.filter((event) => { return id !== event.id; }) ); }; return ( <div className="App"> {events.map((event, index) => ( <div key={event.id}> <h2> {index}-{event.title} </h2> <button onclikc={()=>handleClick(event.id)}></button> //<button onClick={handleClick(event.id)}>Dlt it</button> </div> ))} </div> ); } export default App; ``` 以上直接在 button 裡面觸發 handleClick(event.id) 會導致在網頁初始階段就觸發點擊事件,因此我們可以改成匿名函式,將這個點擊的動作作為參考,直到使用者真正去點擊才觸發。 ![](https://i.imgur.com/nppIemi.png) 但以上做法還有一個問題,就是萬一我們新增了或更動了 title,現有的 handleClick 內處理的方式是 `events.filter((event) =>{return id !== event.id})` , 這段基本上僅有告知電腦如果點擊,確認 id 是否一樣,然後回傳 `id !=== event.id` 的結果,但萬一我們對清單做更新了呢?這樣這個 event handler 並不能有所對應,因為並沒有任何的參數來表示是之前的狀態。 **結論**:如果 state 的更新必須依賴先前的 state,就需要一個 callback function 來抓取先前的狀態 ```javascript= const handleClick = (id) => { setEvents(prevEvent => { return prevEvent.filter(event => { return id !== event.id }) }) } ``` ### Conditional templates - 加入隱藏及顯示列表的功能 - 透過 useState 來表示 true or false 的狀態 ```javascript= import "./App.css"; import { useState } from "react"; function App() { // Conditional templates const [showEvents, setShowEvents] = useState(true); // outputing lists const [events, setEvents] = useState([ { title: "mario's birthday bash", id: 1 }, { title: "bowser's live stream", id: 2 }, { title: "race on moo moo farm", id: 3 }, ]); const handleClick = (id) => { setEvents((prevEvent) => { return prevEvent.filter((event) => { return id !== event.id; }); }); }; return ( <div className="App"> <button onClick={() => setShowEvents(true)}>Show Events</button> <button onClick={() => setShowEvents(false)}>hide event</button> {showEvents && events.map((event, index) => ( <div key={event.id}> <h2> {index}-{event.title} </h2> <button onClick={() => handleClick(event.id)}>Dlt it</button> </div> ))} </div> ); } export default App; ``` 設定初始狀態為 true, 點擊 Show event 時 setShowEvents 的狀態依舊為 true,點擊 hide events 時則變成 false。這樣的隱藏跟顯示狀態必須加入到 map 裡面。 `showEvents && events.map(...)` 意味著只有在 showEvents 成立時,才會接著觸發到後面的隱藏或顯示清單動作 ![](https://i.imgur.com/mbPxiv0.png) 但理想狀態應該是根據目前狀態顯示 show events or hide events 的按鈕,因此我們可以再將程式碼做最佳化,給按鈕的部分也加入條件: ```javascript= import "./App.css"; import { useState } from "react"; function App() { // Conditional templates const [showEvents, setShowEvents] = useState(true); // outputing lists const [events, setEvents] = useState([ { title: "mario's birthday bash", id: 1 }, { title: "bowser's live stream", id: 2 }, { title: "race on moo moo farm", id: 3 }, ]); const handleClick = (id) => { setEvents((prevEvent) => { return prevEvent.filter((event) => { return id !== event.id; }); }); }; return ( <div className="App"> return ( <div className="App"> {!showEvents && ( <button onClick={() => setShowEvents(true)}>Show events</button> )} {showEvents && ( <button onClick={() => setShowEvents(false)}>hide event</button> )} {showEvents && events.map((event, index) => ( <div key={event.id}> <h2> {index}-{event.title} </h2> <button onClick={() => handleClick(event.id)}>Dlt it</button> </div> ))} </div> ); } export default App; ``` - ` {!showEvents && (<button onClick={() => setShowEvents(true)}>Show events</button>)}` 表示如果 showEvents = false ,即清單被隱藏,那 show events 的按鈕就為 true ![](https://i.imgur.com/CE1nxQb.png) - `{showEvents && (<button onClick={() => setShowEvents(false)}>hide event</button>)}` 表示如果 showEvents = true ,即清單被顯示,那 show events 的按鈕就為 false ![](https://i.imgur.com/CuERLgg.png) --- ### Hook 的一些限制 - useState can only be called from the top the lvl of a component - React hook can't be used outside of the component --- ### 加入其他的 component - 在同一個資料夾內建立一個叫做 components 的資料夾(不強制建立),注意,所有的 component 都必須大寫,除了 index.js ![](https://i.imgur.com/ahAv3N8.png) - 建立 Title 的內容 ```javascript= // 寫法一 export default function Title() { return ( <div> <h1 className="title">Mario Kingdom Events</h1> <br /> <h2 className="subtitle">All the latest events in mario Kingdom</h2> </div> ); } // 寫法二 function Title() { return ( <div> <h1 className="title">Mario Kingdom Events</h1> <br /> <h2 className="subtitle">All the latest events in mario Kingdom</h2> </div> ); } export default Title; ``` - 把 Title 加入到其他 component 裡面 - 在最上層先 `import Title from "./components/Title";` - 加入到元件裡面 `<div className="app"><Title /> ... </div>` **注意:** 每一個元件都只能有一個 parent element (就是 app 的 `<div>` element) --- ### Props - props 讓主元件的資料傳送到子元件,這樣的方式,不僅可以統一設計樣式,如果需要更動內容,也只需要在主元件內修正或增加即可,以下是在主元件 App.js 內的程式碼 ```javascript= // 設定 title & subtitle 的變數 const subs = "Check out what do we have now"; const title = "Events in your Area"; // 將變數傳到 <Title /> 裡面 return ( <div className="App"> <Title title={title} subtitle={subs} /> // 如果再增加一個 Title,就可以透過 Title.js 渲染出來 <Title title="xxxxx" subtitle="xxxxx" /> ... </div> ``` - 以下是子元件 Title.js 的程式碼 ```javascript= export default function Title(props) { return ( <div> <h1 className="title">{props.title}</h1> <br /> <h2 className="subtitle">{props.subtitle}</h2> </div> ); } // 透過 Destructuring assignment 解構賦值再最佳化程式碼 export default function Title({title, subtitle}) { return ( <div> <h1 className="title">{title}</h1> <br /> <h2 className="subtitle">{subtitle}</h2> </div> ); } ``` ### React fragment(片段) - 為了避免增加不必要的節點, React 提供 fragment 的方式來去除不必要的 div - 如果 parent element 內沒有使用 prop,可以直接改成 `<>...</>` - 如果 parent element 有使用 prop,div 的部分改成 `<React.fragment></ React.fragment>` --- ### Children props 假設今天我們要為行銷團隊建立一個跳出視窗,裡面可以有不一樣的資訊或折扣等,透過子 props,可以輕鬆達到更換內容,但整體視覺呈現一致性。 - 先在 src 資料夾內的 component 資料夾內建立一個 `Modal.js` 以及 `Modal.css` - 接著我們到 `Modal.css` 來為視窗做簡單的設計。 ```css= /* Modal.css */ .modal-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); } .modal { padding: 30px; max-width: 480px; margin: 200px auto; background: #fff; border-radius: 10px; } ``` - 將 `Modal.css` 匯入到 `Modal.js` - 將 Modal 子元件的內容寫入 ```javascript= // Modal.js import "./Modal.css"; export default function modal() { return ( <div className="modal-backdrop"> <div className="modal"> <h2>20% Off Coupon Code!!</h2> <p>Use the code HELLOWORLD at the checkout.</p> </div> </div> ); } ``` - 匯入到 `App.js` 主元件上 ```javascript= // App.js import Modal from "./components/Modal"; function App() { return ( <div className="App"> // 中間的 code 省略,我們將 Modal 放在最下方 <Modal /> </div> ); } export default App; ``` ![](https://i.imgur.com/0kvqxN3.png) - 現在使用子 prop 的方式重構,在 modal 內透過解構賦值寫入 `{children}` - 在第二個 `div` 內也寫入 `{children}` ```javascript= // Modal.js import "./Modal.css"; export default function modal({children}) { return ( <div className="modal-backdrop"> <div className="modal"> {children} </div> </div> ); } ``` - 在`App.js` 上我們將原本的 `<Modal />` 寫成 tag 的模式 ```javascript= <Modal> <h2>20% Off Coupon Code!!</h2> <p>Use the code HELLOWORLD at the checkout.</p> </Modal> ``` - 以上作法可以在未來在主元件上修改或增加 Modal --- ### Function as prop - 這裡我們來建立一個簡單的將 Modal 關閉的功能 - 首先先在主元件上面建立關閉的 state 並且初始狀態設定為 true,以及一個作為關閉的函式,並將狀態設定為 false ```javascript= // App.js // show modal const [showModal, setShowModal] = useState(true); // handle modal const handleClose = () => { setShowModal(false); }; ``` - 將該函式作為 prop 傳入到 `Modal.js` - 增加一個按鈕來觸發點擊 ```javascript= // Modal.js import "./Modal.css"; export default function modal({ children, handleClose }) { return ( <div className="modal-backdrop"> <div className="modal"> {children} <button onClick={handleClose}>Close</button> </div> </div> ); } ``` - 在主元件 `App.js` 上我們將 showModal 的函式綁定在 Modal 上 - 透過 `{showModal && (<Modal....></Modal>)}` 我們將 Modal 的初始狀態設定為 true(打開),然後透過點擊 Close 按鈕後,狀態就為 false(關閉) ```javascript= // App.js {showModal && ( <Modal handleClose={handleClose}> <h2>20% Off Coupon Code!!</h2> <p>Use the code HELLOWORLD at the checkout.</p> </Modal> )} ``` - 挑戰題:上述的 Modal 會需要更新網頁後才會出現,我們希望: - 網頁載入後 Modal 不顯示 - 讓使用者可以打開及關閉 Modal - 解決方式: - 網頁載入後 Modal 不顯示 => 將初始狀態設定為 false(關閉) - 讓使用者可以打開 Modal => 新增一個按鈕,加入 onclick 事件並設定為 true(開啟) - 原本的按鈕,狀態設定為 false ```javascript= // App.js // 將原本設定 true 的部分改為 false const [showModal, setShowModal] = useState(false) ``` ```javascript= // App.js // 新增一個按鈕,加入 onclick 事件並設定為 true(開啟) {showModal && ( <Modal handleClose={handleClose}> <h2>20% Off Coupon Code!!</h2> <p>Use the code HELLOWORLD at the checkout.</p> </Modal> )} <div> <button onClick={() => setShowModal(true)}>Open Modal</button> </div> ``` ```javascript= // Modal.js // 原本的按鈕,狀態設定為 false return ( <div className="modal-backdrop"> <div className="modal"> {children, handleClose} <button onClick={handleClose}>Close Modal</button> </div> </div> ); ``` --- ### Portal - 上述 Modal 是建立在 parent react component DOM 樹節點上,但一般來說,如果 parent component 有類似如 `overflow:hidden` 或是 `z-index`,需要子元件在需要時打破這個規範,就可以使用 Portal - `ReactDom.createPortal(child, container)` - 改寫上述的 Modal ```javascript= // Modal.js import ReactDom from "react-dom" import "./Modal.css" export default function modal({ children, handleClose }) { return ReactDOM.createPortal( // Child <div className="modal-backdrop"> <div className="modal"> {children} <button onClick={handleClose}>Close</button> </div> </div>, // container document.body ); } ``` - Modal 在點擊 open 之後,會出現在 App.js 的節點之外,但在 body 之內 ![](https://i.imgur.com/kISd5ks.png) - 在未建立 Portal 之前,點擊打開,Modal 會出現在 App.js 的節點內 ![](https://i.imgur.com/EehrrDr.png) - 將 Modal 置於節點外,Modal.css 就會失效,目前所有的字跟按鈕都呈現靠左,這是因為 index.css 內建的設定關係 ![](https://i.imgur.com/Khgb0CU.png) --- ### About styling - **Component stylings are always in global scope, not local.**,因此會發生以下狀況: - 在 `Modal.css` 裡面我們新增一個 `h2` 的客製化樣式,這會影響到其他有 `h2` 的元件上。 - 解決辦法:Adding root class ```javascript= // Modal.js import ReactDOM from "react-dom"; import "./Modal.css"; export default function modal({ children, handleClose }) { return ReactDOM.createPortal( <div className="modal-backdrop"> <div className="modal"> {children} <button onClick={handleClose}>Close</button> </div> </div>, document.body ); } ``` ```css= .modal-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); } .modal { padding: 30px; max-width: 480px; margin: 200px auto; background: #fff; border-radius: 10px; } // 在 h2 前面加上 root 就可以防範其他元件的 h2 也套用到這個樣式 .modal h2 { ... } ``` #### inline-styling - 在 React 裡面使用 inline style,是透過物件的鍵值 - `style={{}}`,第一層 {}是為了要動態改變,第二層的 {} 是物件 - 前面的 css property 是物件的 Key,後面的樣式,需要使用字串,且原本 css 裡面的 - 不能使用,必須改用駝峰式寫法,例如 css 的 `text-align` 在 react 裡面會變成 `textAlign` ```javascript= <div className="modal-backdrop"> <div className="modal" style={{ border: "4px solid", borderColor: "#f8f8", textAlign: "center", }} > {children} <button onClick={handleClose}>Close</button> </div> </div>, ``` --- #### 動態 inline styling - 上面提到 `style={{}}`, 第一個 {} 是可以做動態改變,例如我們給予某些狀況來判斷 borderColor 是粉紅色還是深灰色 - 情境:如果是銷售的 Modal 就呈現灰色,不是就呈現粉紅色 - 設定一個 prop isSaleModal 在主元件,然後將 isSale 傳到 `Moda.js` 來做判斷 ```javascript= // App.js {showModal && ( <Modal handleClose={handleClose} isSaleModal={true}> <h2>20% Off Coupon Code!!</h2> <p>Use the code HELLOWORLD at the checkout.</p> </Modal> )} ``` ```javascript= // Modal.js export default function modal({ children, handleClose, isSaleModal }) { return ReactDOM.createPortal( <div className="modal-backdrop"> <div className="modal" style={{ border: "4px solid", // ? 後為 true : 後為 false // 如果是 isSaleModal 就為"#f8f8" ,不是的話就為"#3333" borderColor: isSaleModal ? "#f8f8" : "#3333", textAlign: "center", }} > {children} <button onClick={handleClose}>Close</button> </div> </div>, document.body ); ``` ![](https://i.imgur.com/dZCg0Rs.png) --- #### Conditional Class - 這裡可以透過條件式來為特定的元件增加 className ```javascript= // Modal.js export default function modal({ children, handleClose, isSaleModal }) { return ReactDOM.createPortal( <div className="modal-backdrop"> <div className="modal" style={{ border: "4px solid", borderColor: isSaleModal ? "#f8f8" : "#3333", textAlign: "center", }} > {children} <button onClick={handleClose} // 如果是 isSaleModal 就建立 slaes-btn 為 className className={isSaleModal ? "sales-btn" : ""} > Close </button> </div> </div>, document.body ); } ``` ```css= .modal .sales-btn { border: 4px solid #333; font-size: 18px; text-transform: uppercase; } ``` ![](https://i.imgur.com/ydDbSP8.png) --- #### module - 建立 css module,檔名規範為 (component name) + . + module + css,例如:`EventList.module.css` - 透過模組建立,匯入到子元件內做客製化樣式 ```css= /* EventList.module.css */ .card { border: 1px solid #eee; box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.05); padding: 10px; max-width: 400px; margin: 20px auto; border-radius: 4px; } ``` ```javascript= // EventList.js import React from 'react'; import styles from "./EventList.module.css"; export default function EventList({ events, handleClick }) { return ( <div> {events.map((event, index) => ( <div className={styles.card} key={event.id}> <h2> {index}-{event.title} </h2> <button onClick={() => handleClick(event.id)}>Dlt it</button> </div> ))} </div> ); } ``` - 問題:如果將這個 card className 加入到其他元件內,會不會有一樣的效果? - 不會!因為模組是 local scope - react 會在每一個使用模組導入的 css 樣式建立一個獨有的識別,從而與其他元件區分 >c1srW 就是該模組的識別碼,而 modal 這個元件內若直接寫入 card 是不會套用效果 ![](https://i.imgur.com/JBkJJNj.png) ![](https://i.imgur.com/MsNjG5q.png) - 但如果客製化元件是以 tagname,就會是 global 的,例如以下: ```css= /* EventList.module.css */ button { background-color: #333; } ``` 全部的按鈕都變成深灰色 ![](https://i.imgur.com/lWs0tF7.png) - 如何避免呢? - 在 button 前加入 .card 讓這個樣式指套用到有 .card className 的元件上 ```css= /* EventList.module.css */ .card button { background-color: #333; } ``` ![](https://i.imgur.com/6PdhnJW.png) --- ## User input and event - 這邊要介紹使用表單 ### Form and label - 透過建立一個表單,可以輸入以及選日期 - 建立一個 `component NewEventForm.js` 和 `NewEventForm.css` - 把表單元件放入 `App.js` 的 Modal 內,由 `showModal` 來控制 Modal 的開啟與關閉 ```javascript= // App.js {showModal && ( <Modal handleClose={handleClose} isSaleModal={false}> <NewEventForm /> </Modal> )} ``` - 在 ` NewEventForm.js` 內建立 onChange 的事件,輸出使用者打的字跟選的日期 ```javascript // NewEventForm.js import React, { useState } from "react"; import "./NewEventForm.css"; export default function NewEventForm() { {/* 建立輸入框跟日期的 useState,並給予一個空字串*/} const [title, setTitle] = useState(""); const [date, setDate] = useState(""); return ( <form className="new-event-form"> {/*使用 label 包覆 input*/} <label> <span>Event Title:</span> <input type="text" onChange={(e) => setTitle(e.target.value)} /> </label> <label> <span>Event Date:</span> <input type="date" onChange={(e) => setDate(e.target.value)} /> </label> <button>Submit</button> {/*輸出使用者輸入的文字和日期*/} <p>Event title: {title}</p> <p>Event date: {date}</p> </form> ); } ``` - 原本應該是需要在 useState 下面建立一個 handleChange 的函式,但因為有兩個事件需要被監聽,因此不採用這種方式,而是直接在 input 裡面以匿名函式來監聽 onChange 的事件 ![](https://i.imgur.com/ZF4VyGM.png) --- ### Controlled inputs - 上述的表單雖然可以透過使用者輸入文字跟日期輸出在底下,但如果我們希望清空這些值,該怎麼做? - 你或許會想到「加一個 reset 的按鈕就好」,那我們來試試看吧! ```javascript= // NewEventForm.js export default function NewEventForm() { const [title, setTitle] = useState(""); const [date, setDate] = useState(""); //建立一個重置的函式 const resetForm = () => { setTitle(""); setDate(""); }; return ( <form className="new-event-form"> <label> <span>Event Title:</span> <input type="text" onChange={(e) => setTitle(e.target.value)} /> </label> <label> <span>Event Date:</span> <input type="date" onChange={(e) => setDate(e.target.value)} /> </label> <p>Event title: {title}</p> <p>Event date: {date}</p> <button style={{ marginRight: "5px" }}>Submit</button> //建立一個按鈕來監聽點擊事件,點擊後就清空輸入的值跟日期 <button onClick={resetForm}>Reset form</button> </form> ); } ``` - 但是結果呈現卻只有清空下面輸出的部分,表單內容還是存在,為什麼呢? ![](https://i.imgur.com/PrttFBg.png) > 原因是我們雖然透過 useState 來取得 title 和 date 的輸入值,卻沒有提供清除這些值的機制,解法就是我們可以透過 input 的 value 來連結清空的函式 ```javascript= // 加上 value <input type="text" onChange={(e) => setTitle(e.target.value)} value={title}/> <input type="date" onChange={(e) => setDate(e.target.value)} value={date} /> ``` ![](https://i.imgur.com/bmzdrIl.png) --- ### Submitting the form - 透過建立 handleSubmit 函式來送出表單,並且給予獨特 id 作為識別用,這邊採用 `Math.random` 來建立 id - handleSubmit 函式不能放在 button,而是要放在 form 上,因為並非 button 觸發表單,而是表單本身。 ```javascript= // NewEventForm.js // 建立 handleSubmit const handleSubmit = (e) => { e.preventDefault(); // 建立新 event obj const event = { title: title, date: date, id: Math.floor(Math.random() * 10000) } // 確認 event 是否正確被發送 console.log(event) //將清除表單輸入框的函式放置最底,submit 完後立即做清除 resetForm(); } ``` - 將 handleSubmit 函式放在 form 表單上 ```javascript= // NewEventForm.js <form className="new-event-form" onSubmit={handleSubmit}> <label> <span>Event Title:</span> <input type="text" onChange={(e) => setTitle(e.target.value)} value={title} /> </label> <label> <span>Event Date:</span> <input type="date" onChange={(e) => setDate(e.target.value)} value={date} /> </label> <button>Submit</button> ``` ![](https://i.imgur.com/kY8hfGp.png) --- ### Adding event to the event list - 發送出去的表單內容加入到先前建立的 event list 裡面。 - 主元件的 event list 狀態要歸零(空的陣列) - 建立一個新的函式來加入舊的及新的 event - 將此函式放置到 <NewEventForm /> - 透過 prop 可以讓子元件 `NewEventForm.js` 使用此函式 - 將此函式加入到子元件的 handleSubmit 函式內 - 新的 event 做為參數傳出並渲染到畫面上 ```javascript= // App,js // 建立增加活動的函式 const addEvent = (event) => { // 舊event setEvent = ((prevEvent) => { // 回傳新的包含新的 event 的陣列 return [...prevEvent, event] }) } // 將該函式放置到子元件上 {showModal && ( <Modal handleClose={handleClose} isSaleModal={false}> <NewEventForm addEvent={addEvent} /> </Modal> )} ``` - `addEvent={addEvent}`可作為 prop 供子元件使用 >The term “render prop” refers to a technique for sharing code between React components using a prop whose value is a function. ```javascript= // NewEventForm.js export default function NewEventForm({ addEvent }) { const [title, setTitle] = useState(""); const [date, setDate] = useState(""); const resetForm = () => { setTitle(""); setDate(""); }; const handleSubmit = (e) => { e.preventDefault(); const event = { title: title, date: date, id: Math.floor(Math.random() * 10000), }; // 新建立好的 event 物件作為 addEvent 的參數 // 到主元件上 addEvent(event); resetForm(); }; ``` ![](https://i.imgur.com/psbQ1jb.png) --- #### 小 bug 修復 - 上面雖然成功地將新的活動渲染出來,但 Modal 並不會在 submit 的時候自動關閉,要點擊先前我們建立的 close 按鈕才能關閉,但我們希望是發送出去後就關閉,因此讓我們來看看哪邊可以調整。 ```javascript= // App.js // 調整一:在 `addEvent` 函式最下方增加控制 Modal 的函式 const addEvent = (event) => { setEvents((prevEvent) => { return [...prevEvent, event]; }); setShowModal(false); }; // 調整二:將原本 <EventForm /> 內的handleClose 清除 // Before {showModal && ( <Modal handleClose={handleClose} isSaleModal={false}> <NewEventForm addEvent={addEvent} /> </Modal> )} // After {showModal && ( <Modal> <NewEventForm addEvent={addEvent} /> </Modal> )} // Modal.js // 調整三:刪除子元件 Modal.js 內的 handleClose prop 以及按鈕 // Before export default function modal({ children, handleClose, isSaleModal }) { return ReactDOM.createPortal( <div className="modal-backdrop"> <div className="modal" style={{ border: "4px solid", borderColor: isSaleModal ? "#f8f8" : "#3333", textAlign: "center", }} > {children} <button onClick={handleClose}>Close</button> </div> </div>, document.body ); //After export default function modal({ children, isSaleModal }) { return ReactDOM.createPortal( <div className="modal-backdrop"> <div className="modal" style={{ border: "4px solid", borderColor: isSaleModal ? "#f8f8" : "#3333", textAlign: "center", }}> {children} </div> </div>, document.body ); ``` --- ### Using useRef - 我們可以透過 useRef 來取得 input 的值,先讓我們大幅調整一下先前的程式碼 ```javascript= //NewEventForm.js // 導入 useRef import React, { useState, useRef } from "react"; // comment out useState // const [title, setTitle] = useState(""); // const [date, setDate] = useState(""); const title = useRef(); const date = useRef(); // comment out the previous code const handleSubmit = (e) => { e.preventDefault(); // 看一下使用 useRef 會得到什麼 console.log(title, date); // const event = { // title: title, // date: date, // id: Math.floor(Math.random() * 10000), // }; // addEvent(event); // resetForm(); }; // comment out onChange and value and use ref={} return ( <form className="new-event-form" onSubmit={handleSubmit}> <label> <span>Event Title:</span> <input type="text" ref={title} // onChange={(e) => setTitle(e.target.value)} // value={title} /> </label> <label> <span>Event Date:</span> <input type="date" ref={date} // onChange={(e) => setDate(e.target.value)} // value={date} /> </label> <button>Submit</button> </form> ); ``` - 當輸入活動跟日期時,會得到 `{current: input}` 的物件 ![](https://i.imgur.com/t0ZgBLH.png) - 繼續往下點會有一個 value 的值 ![](https://i.imgur.com/bRtEi8B.png) ![](https://i.imgur.com/nthC2k7.png) --- - 現在可以完整地用 useRef ```javascript= // NewEventForm.js import React, { useState, useRef } from "react"; import "./NewEventForm.css"; export default function NewEventForm({ addEvent }) { const title = useRef(); const date = useRef(); const resetForm = () => { // 直接將 value 重新賦予空字串 title.current.value = ""; date.current.value = ""; }; const handleSubmit = (e) => { e.preventDefault(); console.log(title, date); const event = { // 透過 current 裡面的 value 取值 title: title.current.value, date: date.current.value, id: Math.floor(Math.random() * 10000), }; addEvent(event); resetForm(); }; return ( <form className="new-event-form" onSubmit={handleSubmit}> <label> <span>Event Title:</span> <input type="text" ref={title} /> </label> <label> <span>Event Date:</span> <input type="date" ref={date} /> </label> <button>Submit</button> </form> ); } ``` --- ### Using Select box - 現在要增加一個可以讓使用者選擇地點的功能以及希望除了活動名稱外,也要將地點、時間一起渲染到畫面 - 在 `NewEventForm.js` 建立一個 location 的 state 以及 select box - submit 出去,input 要清空,但是 select box 維持默認值 Taipei - 到 `EventList.js`,將透過 prop 取得的時間跟地點一起加進去 ```javascript= // NewEventForm.js // 增加 location state // 給予默認值 Taipei const [location, setLocation] = useState("Taipei") // 清空表單但維持默認值 const resetForm = () => { setTitle(""); setDate(""); setLocation("Taipei"); }; // 加入地點到 event 物件 const handleSubmit = (e) => { e.preventDefault(); console.log(title, date); const event = { title: title, date: date, location: location, id: Math.floor(Math.random() * 10000), }; // 在 button 上面增加 select box <label> <span>Event Location:</span> // 監聽 change 的事件並透過 event.target.value 取得改變的值 <select onChange={(e) => setLocation(e.target.value)}> <option value="taipei">Taipei</option> <option value="taoyuan">Taoyuan</option> <option value="taichung">Taichung</option> <option value="kaohsiung">Kaohsiung</option> <option value="pingtung">Pingtung</option> </select> </label> ``` ```javascript= // EventList.js export default function EventList({ events, handleClick }) { return ( <div> {events.map((event, index) => ( <div className={styles.card} key={event.id}> <h2> {index}-{event.title} </h2> <p> // 透過 event 取得地點跟時間 {event.location} - {event.date} </p> <button onClick={() => handleClick(event.id)}>Delete</button> </div> ))} </div> ); } ``` ![](https://i.imgur.com/stEQN1L.png)