7-4 React 中的表單處理(Controlled vs Uncontrolled)

本單元對應的專案分支為:controlled-components-of-form

單元核心#

這個單元的主要目標包含:

  • 了解 Controlled 和 Uncontrolled Components 的概念
  • 了解如何使用 Controlled Components 來操作表單資料

在上一個單元中,使用者已經可以自由的在天氣資訊頁(WeatherCard)和設定頁(Setting)間切換。在這個單元中會說明在 React 中基本的表單處理,讓 React 元件可以知道使用者選擇的天氣選項為何:

Imgur

Controlled vs Uncontrolled Components#

關於表單的處理,我們曾經在網速單位換算器中使用過表單元素 <input type="number" />,當時透過 onChange 搭配 setState 的方式來操作表單的資料,但在當時我們還沒有進一步說明 React 中表單處理的概念。

在 React 中表單元素的處理主要可以分成兩種 Controlled 和 Uncontrolled 這兩種,這裡關於 Controlled 和 Uncontrolled 指的是「資料受不受到 React 所控制」,也就是「受 React 所控制的資料(Controlled)」或「不受 React 所控制的資料(Uncontrolled)」

之所以在表單元素上會區分「受 React 控制的資料」和「不受 React 控制的資料」,主要是因為在瀏覽器中,像是 <input /> 這類的表單元素本身就可以保有自己的資料狀態,這也就是爲什麼當我們在 <input /> 中輸入文字後,可以直接透過 JavaScript 選到該 input 元素後,再取出該元素的值,因為使用者輸入的內容(資料)可以直接保存在 <input /> 元素內。

以下面程式碼舉例來說:

  • 我們可以透過 document.querySelector 選到該表單元素
  • 透過該元素的 value 屬性,就可以知道該 <input /> 欄位中填入的值為何
<input type="text" id="name" />
<script>
const inputName = document.querySelector('#name');
inputName.addEventListener('input', (e) => console.log(e.target.value));
</script>

但到了 React 時,React 就可以幫我們處理資料狀態,我們可以把表單內使用者輸入的資料交給 React,在使用者輸入資料的同時驗證使用者輸入內容的有效性(例如,輸入的內容有誤時跳出提示訊息),並做瀏覽器畫面的更新。

這種把表單資料交給 React 來處理的就稱作 Controlled Components,也就是受 React 控制的資料;相對地,如果不把表單資料交給 React,而是像過去一樣,選取到該表單元素後,才從該表單元素取出值的這種做法,就稱作 Uncontrolled Components,也就是不受 React 控制的資料

針對表單元素, React 會建議我們使用 Controlled Components,基本上使用 Controlled Components 和 Uncontrolled Components 都能達到一樣或類似的效果,但是當我們需要對資料有更多的控制或提示畫面的處理時,使用 Controlled Components 會來得容易的多。

⚠️ 提醒:多數的表單元素都可以交給 React 處理,除了上傳檔案用的 <input type="file" /> 例外,因為該元素有安全性的疑慮,JavaScript 只能取值而不能改值,也就是透過 JavaScript 可以知道使用者選擇要上傳的檔案為何(取值),但不能去改變使用者要上傳的檔案(改值)。因此對於檔案上傳用的 <input type="file" /> 只能透過 Uncontrolled Components 的方式處理

在有了 Controlled 和 Uncontrolled Components 的概念後,現在就來讓我們看看如何實作這兩者表單處理的方式。

在這個單元中我們會先練習 Controlled Components 的表單處理,下一個單元再來看看,如果改成 Uncontrolled Components 的話可以怎麼做。不論是使用 Controlled 或 Uncontrolled 的方法,當使用者在天氣設定頁點選「儲存」的時候,都可以在瀏覽器的開發者工具中看到使用者欲儲存的地區,但實際上資料的保存則會到後面的單元再繼續說明。

Controlled Components#

針對表單元素,React 會建議我們使用 Controlled Components,也就是把表單的資料儲存在該 React 元件內交給他來處理,這個做法就和先前網速單位換算器中使用的方式相同。

將資料交給 React#

因為要將資料交給 React 處理,所以會先透過 useState 來建立保存資料狀態的地方,接著在表單元素上透過 onChange 事件來取得該表單元素當前的值,並且馬上更新到 React 元件的資料狀態內。

套用到 WeatherSetting 天氣設定頁面就像這樣:

  1. 從 react 中載入 useState
  2. 透過 useState 取得 locationNamesetLocationName,將預設值先設為「臺北市」:
// ./src/views/WeatherSetting.js
// STEP 1:從 react 中載入 useState
import React, { useState } from 'react';
// ...
const WeatherSetting = ({ handleCurrentPageChange }) => {
// STEP 2:定義 locationName,預設值先帶為空
const [locationName, setLocationName] = useState('臺北市');
// ...
};

⚠️ 提示:這裡不把資料狀態取名為 location 的原因在於瀏覽器本身就有一個 window.location 物件,因此當你直接在瀏覽器的 console 面版中輸入 location 是會得到內容的,為了避免可能的錯誤,取名為 locationName

  1. 使用 onChange 事件來監聽使用者輸入的資料,並且當事件觸發時呼叫 handleChange
const WeatherSetting = ({ handleCurrentPageChange }) => {
// ...
return (
// 使用 onChange 搭配 handleChange 來監聽使用者輸入的資料
<StyledSelect id="location" name="location" onChange={handleChange}>
{/* ... */}
</StyledSelect>
);
};
  1. 定義 handleChange 函式,當使用者輸入資料時,把資料內容透過 setLocation 更新 React 內部的資料狀態

  2. 把使用者輸入的內容透過 setLocationName 更新到 React 內的資料狀態

const WeatherSetting = ({ handleCurrentPageChange }) => {
const [locationName, setLocationName] = useState('臺北市');
// STEP 4:定義 handleChange 要做的事
const handleChange = (e) => {
console.log(e.target.value);
// STEP 5:把使用者輸入的內容更新到 React 內的資料狀態
setLocationName(e.target.value);
};
};

現在當使用者點選某一個地區時,我們將可以從 React Developers Tools 中看到使用者選擇的項目:

Imgur

帶入資料預設值#

透過 React Developer Tools 你會發現,當使用者一進來這頁時,因為 locationName 的預設值是空字串,因此除非使用者有點擊地區選項,否則雖然顯示 React Developers Tools 中資料「臺北市」,但畫面並沒有連帶對應。但如果使用者前一次已經有儲存過地區資料,當使用者在進到設定頁的時,地區欄位應該就會直接帶出上一次他填寫的資訊,在 Controlled Components 中可以怎麼做呢?

若要讓這個地區欄位的 <input> 在頁面呈現時就帶有預設值的話,我們可以直接在 <input> 中加上 value 屬性,React 會自動把這個 value 帶入的值當作該欄位的預設值呈現出來:

const WeatherSetting = ({ handleCurrentPageChange }) => {
const [locationName, setLocationName] = useState('臺北市');
return (
<StyledSelect
id="location"
name="location"
onChange={handleChange}
// 透過 value 可以讓資料與畫面相對應
value={locationName}
>
{/* ... */}
</StyledSelect>
);
};

建立點擊儲存後的行為流程#

現在當使用者點擊儲存按鈕時,我們只需要把使用者選擇的項目透過 console.log 顯示出來,在後面的單元再來實作儲存的功能,因此:

  1. 定義 handleSave 函式
  2. 在儲存按鈕透過 onClick 綁定事件,並觸發 handleSave 方法
const WeatherSetting = ({ handleCurrentPageChange }) => {
// ...
// STEP 1:定義 handleSave 函式
const handleSave = () => {
console.log('locationName', locationName);
};
return (
// ...
<ButtonGroup>
{/* STEP 2:點擊儲存按鈕時,觸發 handleSave */}
<Save onClick={handleSave}>儲存</Save>
</ButtonGroup>
);
};

這時候當使用者點擊儲存時,即可在瀏覽器開發者工具的 console 面板看到使用者欲儲存的資料:

Imgur

換你了!取得使用者欲儲存的地區資訊#

在這個單元中我們說明了 controlled components 的做法,也就是透過 useState,表單中的 onChangevalue 來達到把資料交給 React 內部的資料狀態進行管理,最後當使用者點擊「儲存」按鈕時,可以將使用者欲保存的地區資訊顯示在 console 面板中。

現在換你實際練習看看:

  • 透過 useState 取得 locationNamesetLocationName 方法,預設值為「臺北市」
  • 定義 handleChange 方法,當表單的資料改變時,把最新的資料透過 setLocationName 保存在 React 內部的資料狀態中
  • onChange 事件綁定在 select 元素上,當資料改變時觸發 handleChange 方法
  • 定義 handleSave 方法,單純先把 locationName 的資料 console 出來
  • 當使用者點擊儲存時,觸發 handleSave 方法

在下一個單元中,會接著說明如何用 uncontrolled components 來達到相同的功能。

本單元相關之網頁連結、完整程式碼與程式碼變更部分可於 controlled-components-of-form 分支檢視:https://github.com/pjchender/learn-react-from-hook-realtime-weather-app/tree/controlled-components-of-form

Imgur