###### tags: `React` `state` `props` # [week 21] 前端框架 - 先別急著學 React > 本篇為 [[FE302] React 基礎 - hooks 版本](https://lidemy.com/p/fe302-react-hooks) 這門課程的學習筆記。如有錯誤歡迎指正! ``` 學習目標: P1 我知道 React 的目的以及原理 P1 我知道我們為什麼需要 React P1 我知道使用 React 跟之前使用 jQuery 的區別 P1 我理解 state 跟 props 的不同 ``` --- ## 先別急著學 React 在開始之前,我們先來複習如何用 jQuery 來做出一個簡單的 Todo List 吧! 1. 以下是套用 Bootstrap 切的版型,不過切版部分不是這次要探討的重點: ```htmlmixed= <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous"> ``` <iframe height="324" style="width: 100%;" scrolling="no" title="[week21] Todo List template" src="https://codepen.io/heidiliu2020/embed/preview/vYKoNJK?height=324&theme-id=dark&default-tab=html,result" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true"> See the Pen <a href='https://codepen.io/heidiliu2020/pen/vYKoNJK'>[week21] Todo List template</a> by Heidi-Liu (<a href='https://codepen.io/heidiliu2020'>@heidiliu2020</a>) on <a href='https://codepen.io'>CodePen</a>. </iframe> 2. 接著引入使用 jQuery: ```javascript= <script src="http://code.jquery.com/jquery-latest.js"></script> ``` 3. 就可以來新增功能了: - 新增 - 切換已完成/未完成 - 刪除 <iframe height="265" style="width: 100%;" scrolling="no" title="[week21] Todo List template" src="https://codepen.io/heidiliu2020/embed/preview/vYKoNJK?height=265&theme-id=dark&default-tab=js,result" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true"> See the Pen <a href='https://codepen.io/heidiliu2020/pen/vYKoNJK'>[week21] Todo List template</a> by Heidi-Liu (<a href='https://codepen.io/heidiliu2020'>@heidiliu2020</a>) on <a href='https://codepen.io'>CodePen</a>. </iframe> ## 初探 Component 根據 [React](https://zh-hant.reactjs.org/docs/components-and-props.html) 官網定義: > Component 使你可以將 UI 拆分成獨立且可複用的程式碼,並且專注於各別程式碼的思考。 簡單來說,component(元件、組件)其實就像 JavaScript 的 function,可接收任意的 props(參數, property 的簡寫)並且回傳描述畫面的 React element。 以我們剛才實作的 todolist 來說,就可以根據功能分成不同的 component: ![](https://i.imgur.com/XYSTyfM.png) ### Component & Props 再來我們可試著用 component 的概念改寫程式碼,例如切割出 Todo 和 Button component。 1. 必須用英文大寫開頭命名來辨別是 component:   ```javascript= // Todo component: 用來回傳 html 內容, 用大寫英文開頭 function Todo(content) { return ` <li class="todo list-group-item d-flex justify-content-between align-items-center"> <div class="todo-title ">${content}</div> <div class="btn-group"> ${Button('btn-undone btn-outline-secondary', '未完成')} ${Button('btn-delete btn-outline-danger', '刪除')} </div> </li> ` } // Button component function Button(className, content) { return ` <button class="btn ${className}" type="button">${content}</button> ` } ``` 2. 監聽事件就可改成 Button component 寫法,比起原本的 HTML 程式碼,用 component 寫法會更容易閱讀: ```javascript= $('.todos').on('click', '.btn-undone', e => { const todo = $(e.target).parent(); e.target.remove(); todo.prepend( Button('btn-done btn-outline-success', '已完成') ); }); $('.todos').on('click', '.btn-done', e => { const todo = $(e.target).parent(); e.target.remove(); todo.prepend( Button('btn-undone btn-outline-secondary', '未完成') ); }); ``` 3. 但其實這個寫法還能再做優化,像是 Button() 如果要再傳入更多參數,就會很難快速判斷。因此我們可以改成傳入物件,也就是 props(參數)來簡化: ```javascript= // Todo component: 傳入物件作為參數 function Todo(props) { return ` <li class="todo list-group-item d-flex justify-content-between align-items-center"> <div class="todo-title ">${props.content}</div> <div class="btn-group"> ${Button({ className: 'btn-undone btn-outline-secondary', content: '未完成' })} ${Button({ className: 'btn-delete btn-outline-danger', content: '刪除' })} </div> </li> ` } // Button component: 傳入物件作為參數 function Button(props) { return ` <button class="btn ${props.className}" type="button">${props.content}</button> ` } ``` 監聽事件也同樣以物件的形式修改,完整程式碼與執行畫面如下: <iframe height="265" style="width: 100%;" scrolling="no" title="[week21] Todo List template_component" src="https://codepen.io/heidiliu2020/embed/preview/dypbKLw?height=265&theme-id=dark&default-tab=js,result" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true"> See the Pen <a href='https://codepen.io/heidiliu2020/pen/dypbKLw'>[week21] Todo List template_component</a> by Heidi-Liu (<a href='https://codepen.io/heidiliu2020'>@heidiliu2020</a>) on <a href='https://codepen.io'>CodePen</a>. </iframe> 透過這種寫法,我們能夠將重複的區塊變成模板,建立統一的規則來封裝,也會提高程式碼的可讀性。 但 component 其實是一種抽象的概念,還有另外一種較為標準的寫法 [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components),類似建立新的 HTML 標籤來使用,只是目前瀏覽器支援度還不高,這部分我們並不進行討論。 ## 資料 vs 畫面(UI) 根據前面的範例,是把資料放到畫面上來呈現,也就是在新增、修改 todo 時,同時去更動資料和畫面。因此我們可從 UI 去抽取資料,但我們無法直接把 HTML 程式碼存到資料庫,而是要去除標籤,轉換成 JSON 等最原始的資料形式。 此外,透過這種方式其實有個壞處,假如有部分資料沒有更動到,就可能導致資料與畫面不一致。 ![](https://i.imgur.com/TKkUPnR.png) 至於要如何解決這個問題,我們可以改成直接修改資料,然後畫面從資料產生,即可保持資料與畫面的一致性。 ![](https://i.imgur.com/k1STU0n.png) 這其實就是 React 第二個重要概念 State,畫面永遠由 state 產生。用數學式可表示成 `UI = f(state)`,代表 state 不變的話,透過 state 產生的 UI 也不會改變。 ## 畫面永遠都由 state 產生 根據 [React](https://zh-hant.reactjs.org/docs/react-component.html#state) 官網定義: > State 包含了某個 component 內特定的、會隨時間改變的資料,這個 state 是由使用者定義的。它應是一個簡單的 JavaScript object。 > 如果某個值並沒有在 render 或資料流中被使用(例如計時器的 ID),你不需要將它放在 state 內。 繼續舉剛才的範例,我們可以把 todos 放到 state 物件中,並且將 Todo component 中的 todo 加上 id: ```javascript= let id = 0; let state = { todos: [] } function Todo(props) { // 自訂屬性通常以 data- 開頭 return ` <li class="data-id="${id}"> ... ` ``` - 新增 todo:透過 updateState() 來更新資料 ```javascript= $('.btn-add').click(() => { const content = $('.input-todo').val(); if (!content) return; $('.input-todo').val(''); // 更新 state updateState({ todos: [...state.todos, { id, content, isDone: false }] }); id++; }); ``` - 呼叫 updateState() 來更新資料,再根據資料 render 出畫面: ```javascript= // 更新 state function updateState(newState) { state = newState; render(); } function render() { // 先把畫面清空 $('.todos').empty(); $('.todos').append( // 把每個 todo 的 HTML 集合起來放到畫面上 state.todos.map(todo => Todo(todo)).join('') ); } ``` - 刪除 todo:透過篩選掉資料中相對應的 todo 來更新 state ```javascript= $('.todos').on('click', '.btn-delete', e => { // 讀取該 todo id const id = Number($(e.target).parents('.todo').attr('data-id')); // 更新 state: 篩選掉資料中相對應的 todo updateState({ todos: state.todos = state.todos.filter(todo => todo.id !== id) }); }); ``` 修改 todo 狀態也是類似概念,完整程式碼如下: <iframe height="265" style="width: 100%;" scrolling="no" title="[week21] Todo List template_component &amp; state" src="https://codepen.io/heidiliu2020/embed/preview/wvzvBbY?height=265&theme-id=dark&default-tab=js,result" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true"> See the Pen <a href='https://codepen.io/heidiliu2020/pen/wvzvBbY'>[week21] Todo List template_component &amp; state</a> by Heidi-Liu (<a href='https://codepen.io/heidiliu2020'>@heidiliu2020</a>) on <a href='https://codepen.io'>CodePen</a>. </iframe> --- ## 結論 在剛接觸 React 時,覺得和之前實作的 [SPA 留言版](https://hackmd.io/@Heidi-Liu/note-be101-php-todolist)概念很類似,以非同步方式串接後端 API,透過將前後端分離,以動態方式來更新頁面,同樣是直接更新後端資料來顯示前端畫面。 我們也從 Todo List 範例中,學到 React 最重要的兩個概念 Component 和 State: - 透過 Component 將畫面與功能模組化,並以傳入的參數 props 來設定屬性或是資料 - 因為畫面永遠由 State 產生,我們會直接更改資料,再由資料去顯示畫面 瞭解到這些概念後,我們就能正式入門 React 這套 JavaScript Library。