iT邦幫忙

2022 iThome 鐵人賽

DAY 10
1
Modern Web

終究都要學 React 何不現在學呢?系列 第 10 篇

終究都要學 React 何不現在學呢? - React 基礎 - TodoList (上) - (10)

  • 分享至 

  • xImage
  •  

前言

接下來這一章節我們將會實作一些小東西,主要是要整合前面的知識點,否則單看知識而沒有任何一點輸出的話是沒有任何用處的,因此就讓我們準備練習一下吧。

範本樣板

首先這邊我用 TailwindCSS 做了一個 ToDoList 範本,而這個範本主要會預先載入以下 CDN

因此會建議你往下看之前,可以先 Fork 回去到你自己的 CodePen 中,等一下你才方便實作,這裡面也包含了基本 HTML 樣板

ToDoList 範本

React ToDoList

接下來我們將會實作幾個功能

  1. 輸入代辦事項,點擊「+」後出現在下方
  2. 完成的代辦事項可以打勾完成
  3. 可顯示幾筆代辦事項
  4. 清空全部任務(不論完成與否都清除)

初始化 React

接下來就讓我們開始實作,首先先將基本的 React 建立出來,然後這邊可以先將 #root 裡面的元素先全部丟進去 React return 中,接著你必須修正幾個錯誤,否則會無法正常運作

  • 第一個就是 JSX 本身是 JavaScript 擴充,因此 class 屬於 JavaScript 保留字,因此 class 要改成 className
  • 第二個也就是 Expected corresponding JSX closing tag for <input>,這個錯誤簡單來講就是 JSX 比較嚴謹,因此要調整 input 補上結尾標籤(也就是補上 /)
const App = () => {
  return (
    <div>
      <div className="bg-indigo-500 p-5 h-screen">
        <div className="max-w-[768px] m-auto bg-white p-5">
          <h1 className="text-center text-2xl mb-4">React ToDoList</h1>
          <div className="flex">
            <input type="text" className="w-full rounded-l-lg border-l-2 border-y-2 border-indigo-300 pl-4 focus:outline-indigo-500 focus:outline-none focus:outline-offset-0" placeholder="請輸入你的代辦事項" />
            <button className="w-[50px] h-[50px] border-0 bg-sky-500 hover:bg-sky-600 rounded-r-lg text-white transition duration-700">+</button>
          </div>
          <ul>
            <li className="py-4">
              <label>
                <input type="checkbox" />
                今天要洗碗
              </label>
            </li>
          </ul>
          <div className="flex justify-between items-center">
            <p>
              目前有 <span className="font-medium">1</span> 個事項待完成
            </p>

            <button type="button" className="bg-red-300 p-2 rounded-md hover:bg-red-400 transition duration-700">Clear All Task</button>
          </div>
        </div>
      </div>
    </div>
  )
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

CodePen 連結

如果你有依照以上步驟調整,你會發現 React 已經可以正常運作了,接著我們就可以開始實作功能了

宣告資料狀態

接下來要宣告一個初始資料狀態,是要拿來放 todo 新增的資料,這邊要注意的是初始資料會是一個陣列

const [ todoList, setTodoList ] = React.useState([]);

CodePen 連結

點擊事件綁定

那麼接下來我們要先替幾個東西綁定事件,也就是點擊事件,第一個是「+」按鈕,而這個事件我們先對應到一個函式叫做 addTodo

<button onClick={ addTodo } className="w-[50px] h-[50px] border-0 bg-sky-500 hover:bg-sky-600 rounded-r-lg text-white transition duration-700">+</button>
const addTodo = () => {
}

第二個則是 「Clear All Task」 按鈕,對應的是 remoteAllTodo

<button onClick={ remoteAllTodo } type="button" className="bg-red-300 p-2 rounded-md hover:bg-red-400 transition duration-700">Clear All Todo</button>
const remoteAllTodo = () => {
}

CodePen 連結

第三個就是勾選 todo 狀態的 checkbox,但是這邊我們先不綁事件,先把上面這兩個綁好就好,等一下後面再來一起處理。

取得 input 的值

第一個我們先來寫新增代辦的函式,當我們觸發 onClick 事件之後,要去取得 input 的 value 因此要稍微調整一下 input 給予一個 id

<input id="todoInput" type="text" className="w-full rounded-l-lg border-l-2 border-y-2 border-indigo-300 pl-4 focus:outline-indigo-500 focus:outline-none focus:outline-offset-0" placeholder="請輸入你的代辦事項" />

接著改寫一下 addTodo 函式

const addTodo = (event) => {
  const value = document.querySelector('#todoInput').value;
  console.log(value);
};

CodePen 連結

接下來你在 input 隨便輸入並按下「+」 就可以看到我們正確取得 input 的值了。

儲存 input 的值

當我們取得值之後,接下來就要將值推進去到 setTodoList 中,首先第一個會是解構原本資料的物件資料,第二個我們則要寫入另一個物件

const addTodo = () => {
  const input = document.querySelector('#todoInput');
  setTodoList([
    ...todoList,
    {
      name: input.value
    }
  ])
};

之所以要先解構原本資料是因為我們不想要直接覆蓋掉原本的資料,而是要將新的資料推進去,這樣才不會造成資料的遺失,因此才會需要先解構原本的資料,接著再推進去新的資料。

接下來我們也要寫入代辦新增的時間,時間最簡單方式就是使用 Date.now(),剛好這個 Date.now() 非常適合作為一個 Key 使用,因此就會直接將它作為一個 Key 使用以及最後補個這個代辦事項的狀態(已完成與未完成)

const addTodo = (event) => {
  const input = document.querySelector('#todoInput');
  setTodoList([
    ...todoList,
    {
      id: Date.now(),
      name: input.value,
      status: false,
    }
  ])
};

最後別忘記要清空 input 的欄位,畢竟新增成功就要清空

const addTodo = (event) => {
  const input = document.querySelector('#todoInput');
  setTodoList([
    ...todoList,
    {
      id: Date.now(),
      name: input.value,
      status: false,
    }
  ])
  input.value = '';
};

CodePen 連結

渲染代辦事項

接下來就是要渲染我們儲存 todoList 的代辦事項資料,如果是在 Vue 裡面我們會使用 v-for 來渲染畫面,而在 React 則是使用 map 並重新組合成一個 HTML 回傳給 JSX 來渲染資料

因此要將這一塊

<ul>
  <li className="py-4">
    <label>
      <input type="checkbox" class="mr-2" />
      今天要洗碗
    </label>
  </li>
</ul>

改成使用 map 組合 DOM,記得不要忘記 key 的存在,(記得補一個 checked)

<ul>
  {
    todoList.map((todo) => (
      <li className="py-4" key={ todo.id }>
        <label>
          <input type="checkbox" class="mr-2" checked={ todo.status }/>
          { todo.name }
        </label>
      </li>
    ))
  }
</ul>

你可能會想說為什麼使用 map 就可以正常渲染資料?在前面章節我們有說過 JSX 本身會針對陣列展開 (Spread),而 map 會回傳一個陣列,因此就會正常渲染資料。

接著這邊你也可以順便把下方這一段改一下

<p>
  目前有 <span className="font-medium">1</span> 個事項待完成
</p>

改成以下這樣就可以了

<p>
  目前有 <span className="font-medium">{ todoList.length }</span> 個事項待完成
</p>

CodePen 連結

基本上到了這一步驟之後,你就可以開始試著新增一個代辦事項,然後你也可以在下方看到資料被渲染出來囉。

checkbox 事件綁定

那麼接下來我們要增加當使用者勾選任務後,會補上一個刪除線的效果,如同底下

這是一條刪除線

而判斷方式非常簡單,由於我們是使用 TailwindCSS 來實作,因此在 className 上面補上一個判斷,當 state 為 true 的話,就會自動加上 line-through 樣式即可。

只是在開始之前我們要先針對 label 增加一個屬性,也就是 data-* 並傳入 key,這個我們稍後會使用到

<ul>
  {
    todoList.map((todo) => (
      <li className="py-4" key={ todo.id }>
        <label className={ todo.status ? 'line-through' : ''}>
          <input type="checkbox" data-id={ todo.id } className="mr-2" checked={ todo.status }/>
          { todo.name }
        </label>
      </li>
    ))
  }
</ul>

這時候你新增每一筆資料就都可以看到 Li 多了一個 data-id 的屬性。

接下來也要針對 label 綁定一個事件,也就是 onClick 事件,對應到 updateTodo 方法

<ul>
  {
    todoList.map((todo) => (
      <li className="py-4" key={ todo.id } data-id={ todo.id } >
        <label className={ todo.status ? 'line-through' : ''} >
          <input onClick={ updateTodo } type="checkbox" className="mr-2" data-id={ todo.id } checked={ todo.status }/>
          { todo.name }
        </label>
      </li>
    ))
  }
</ul>

而 updateTodo 會透過 event.target 去尋找 data-* 然後用這種方式去找出資料中符合 ID 的部分,並將該陣列資料轉換為 false or true

const updateTodo = (event) => {
  const { id } = event.target.dataset;
  const newTodoList = todoList.map((todo) => {
    if(todo.id === Number(id)) {
      todo.status = !todo.status;
    }
    return todo;
  });

  setTodoList([ ...newTodoList ]);
}

由於我們這邊解構出來的 dataset 都是一個字串,因此要記得補上 Number 做型別轉換。

接著這時候你可以嘗試輸入代辦事項並新增代辦,然後在打開 Console 應該會發現這個錯誤

Warning: You provided a checked prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultChecked. Otherwise, set either onChange or readOnly.

而這有幾種解決方式,將原本的 checked 改成 defaultChecked,或是在 input 上面增加一個 onChange 事件,這邊我們選擇後者。

<ul>
  {
    todoList.map((todo) => (
      <li className="py-4" key={ todo.id } data-id={ todo.id } >
        <label className={ todo.status ? 'line-through' : ''} >
          <input onChange={ updateTodo } type="checkbox" className="mr-2" data-id={ todo.id } checked={ todo.status }/>
          { todo.name }
        </label>
      </li>
    ))
  }
</ul>

CodePen 連結

這樣子就不會再出現該警告訊息了。

清空 Todo

恭喜你做到這邊的時候就已經將大部分功能給完成了!

所以最後就是單純的去撰寫 remoteAllTodo 裡面的功能,而這也其實非常簡單,因為我們只是要將代辦事項清空,因此只需要這樣寫即可

const remoteAllTodo = () => {
  setTodoList([]);
};

CodePen 連結

非常簡單吧?你的第一個簡單版 React TodoList 就這樣完成了。

這邊也提供完整版 TodoList CodePen 的連結。

除此之外我也提供一版 Vue 版本的 TodoList CodePen 可以讓你比較一下兩者的差異。

後記

本文將會同步更新到我的部落格


上一篇
終究都要學 React 何不現在學呢? - React 基礎 - useContext - (9)
下一篇
終究都要學 React 何不現在學呢? - React 基礎 - TodoList (下) - (11)
系列文
終究都要學 React 何不現在學呢? 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言