iT邦幫忙

2022 iThome 鐵人賽

DAY 19
0

本文將以實作To Do List理解Listing State Up
包含以下部分

  • component拆分—圖解
  • lifting-state說明
  • 重要概念
  • 具體程式碼
  • 小結—Listing State Up造成什麼問題

component拆分—圖解

我們拆分App、AppToDo、ToDoItem、ListToDo這幾個component以下針對component要做的事情做簡單的說明

  • AddTodo:
    • 提供輸入代辦事項的input
    • 輸入完input後按下 **add **即可新增事項
  • ListToDo:
    • 渲染所有的代辦事項
  • ToDoItem:
    • 顯示個別的代辦事項
    • 按下delete即可刪除該代辦事項
    • 按下勾選框即可在代辦事項內容顯示刪除線
  • App:
    • 包含整個代辦事項的應用程式

具體呈現如下圖每個顏色的線框表示一個component

lifting-state說明

操作說明

根據react官方提升state的解釋,當我們有一些component擁有相同的資料需要變化的時候,建議使用共享的state提升到最靠近的共同ancestor

以本範例當中,我們會有共同的state變數叫做listData,listData是一個所有代辦事項內容的array,其中這些component都依賴此state,我們會在App這個祖層(ancestor)的component宣告state,然後透過props傳遞。

重要概念

  • single source of truth
  • top-down data flow

lifting State底下的官方說明如下

There should be a single “source of truth” for any data that changes in a React application. Usually, the state is first added to the component that needs it for rendering. Then, if other components also need it, you can lift it up to their closest common ancestor. Instead of trying to sync the state between different components, you should rely on the top-down data flow.

以這個範例而言,我們將資料建立在App component,資料由上而下透過props傳遞到ToDoItem和AddToDo,這些元件仰賴的來源是App元件所建立的state。也就符合了single source of truthtop-down data flow

資料夾結構

具體建立資料夾可以如下的方式
components資料夾建立ToDo資料夾
其中包含AddToDoListToDoToDoItem以及index.js

Todo裡面的index.js僅彙整component再export讓App.js做引入

在toDo資料夾底下的index.js程式碼如下

export {default as AddToDo} from './AddToDo.jsx';
export {default as ListToDo} from './ListToDo.jsx';

資料夾結構如下

│  App.js
│  index.js
└─components
    └─Todo
        AddToDo.jsx
        index.jsx
        ListToDo.jsx        
        ToDoItem.jsx  

具體程式碼

App檔案

透過state lifting的方式將**state宣告在共同祖層(ancestor)**的app.js

import React, { useState } from 'react'
import { AddToDo, ListToDo } from './components/Todo'
const App = () => {
  //將listData state宣告在上層
  const [listData, setListData] = useState([]);
  return (
    <div >
      <AddToDo setListData={setListData} />
      <ListToDo listData={listData} setListData={setListData} />
    </div>
  )
}
export default App

AddToDo檔案

props接收從父層App component的setListData,如果使用者輸入完畢按下新增按鈕來新增代辦事件的時候呼叫setListData

import { useState } from 'react';
const AddToDo = ({ setListData }) => {
  //in-line style的部分
  const margin0Auto = { width: "300px", margin: "0 auto" };
  const textAlign = { textAlign: "center" };

  const [input, setInput] = useState("");

  const inputChange = (e) => {
    setInput(e.target.value);
  }

  const addHandler = () => {
    //避免輸入空白
    if (input.trim() === "") { return }
    //來自App component的setListData函式
    setListData(
      (prev) => ([...prev, {
        content: input,
        id: Date.now(),
        done: false
      }])
    )
    //按下新增後清空input
    setInput('');
  }

  return (
    <div style={{ ...textAlign, ...margin0Auto }}>
      <input type="text" value={input} onChange={inputChange} />
      <button onClick={addHandler}>add</button>
    </div>
  )
}
export default AddToDo

ListToDo檔案

這個component只負責遍歷listData內容來渲染<ToDoItem/>元件並將所需要用到的props和setListData繼續往下傳給子層ToDoItem元件。

import React from 'react'
import ToDoItem from './ToDoItem';
const ListToDo = ({ listData, setListData }) => {
  //in-line style的部分
  const margin0Auto = { width: "300px", margin: "0 auto" };
  //使用map來render<ToDoItem /> 並將listData這個array裡面的item屬性透過props傳遞給ToDoItem
  return (
    <ul style={margin0Auto} >
      {listData.map((data) => {
        return <ToDoItem setListData={setListData} key={data.id} content={data.content} id={data.id} done={data.done} />
      })}
    </ul >
  )
}
export default ListToDo

ToDoItem檔案

接收來自ListToDo的props其中包含用來更新ListData狀態setListData

import React from 'react'
const ToDoItem = ({ id, content, setListData, done }) => {
  const margin10 = { margin: "10px" };
  const displayFlex = { display: "flex", justifyContent: "center", alignItems: "center" };
  const displayBlock = { display: "block" };
  const deleteHandler = () => {
    setListData((prev) => (prev.filter(item => item.id !== id)))
  }
  const completeHandler = (id) => {
    setListData(prev => {
      return prev.map(item => {
        if (item.id === id) {
          item.done = !item.done;
        }
        return item;
      })
    });
  }
  return (
    <li style={{ ...margin10, ...displayFlex }}>
      <input onChange={() => completeHandler(id)} type="checkbox" />
      <p style={{ textDecoration: done ? "line-through" : "none" }}>
        {content}
      </p>
      <button onClick={deleteHandler} style={{ ...margin10, ...displayBlock }}>delete</button>
    </li>
  )
}
export default ToDoItem

最後應當可以看到如下圖

小結—Listing State Up造成什麼問題

這邊可以發現ListToDo並沒有實質使用setListData函式,因為其子層元件ToDoItem需要使用的原因,所以得透過props接收來自App.js後再次透過props轉傳給子層的ToDoItem,這邊僅傳遞兩層,如果遇到其共通ancestor需傳遞給子元件過多層數時候,此一現象稱之為props drilling

另外在處理addHandler、completeHandler、deleteHandler的邏輯程式碼都會寫在其component裡面,為了改善上述兩種情形此系列(中)將搭配使用useContextuseReducer

參考資料

上一篇
為什麼useReducer,reducer詞彙解釋—用流程圖解釋useReducer
下一篇
從實作To Do List理解useContext搭配useReducer運作模型—(附圖)(中)
系列文
從Create到React—用來實作使用者介面的JavaScript函式庫30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言