概述
React 中的受控組件和非受控組件都是針對于表單數據而言的。React 推薦使用受控組件來處理表單數據。
在受控組件中,表單數據由 React 組件的 state 管理。在非受控組件中,表單數據交由 DOM 節點處理,可以使用 ref 來從 DOM 中獲取表單數據。
受控組件
React 的 state 是表單的數據源,并且渲染表單的 React 組件還控制著用戶輸入過程中表單發生的操作。
function Form() {
const [name, setName] = useState('');
const onInputChange = event => setName(event.target.value);
const onFormSubmit = event => {
console.log('提交的數據 => ', name);
event.preventDefault();
};
return (
<form onSubmit={onFormSubmit}>
<label>
名字:
<input type='text' value={name} onChange={onInputChange} />
</label>
<input type='submit' value='提交'/>
</form>
)
}
非受控組件
表單元素自己維護 state。可以使用 ref 獲取表單數據
function Form() {
const inputElement = useRef(null);
const onFormSubmit = event => {
console.log('提交的數據 => ', inputElement.current.value);
event.preventDefault();
};
return (
<form onSubmit={onFormSubmit}>
<label>
名字:
<input type='text' ref={inputElement} />
</label>
<input type='submit' value='提交'/>
</form>
)
}
默認值
在 React 渲染生命周期時,表單元素上的 value
將會覆蓋 DOM 節點中的值,在非受控組件中,React 可以通過 defaultValue
屬性賦予組件一個初始值。
return (
<form onSubmit={onFormSubmit}>
<label>
名字:
<input type='text' defaultValue='test' ref={inputElement} />
</label>
<input type='submit' value='提交'/>
</form>
)
具有默認值(defaultValue)的表單元素,此時是否應該去控制后續的更新?
特殊的表單元素
<textarea>
在 React 中,<textarea>
使用 value
屬性代替 HTML 中的子元素定義其文本。
<textarea value={value} onChange={handleChange} />
<select>
React 使用根 <select>
標簽上的 value
屬性定義選中,而不使用 selected
屬性。
<select value={value} onChange={handleChange}>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>
<input type='file'>
React 中 <input type='file'>
始終是一個非受控組件,因為它的值只能由用戶設定,而不能通過代碼控制。
受控組件與非受控組件的選擇
功能 | 受控組件 | 非受控組件 |
---|---|---|
一次性的取值(例如:提交時) | ? | ? |
提交時驗證 | ? | ? |
實時驗證 | ? | ? |
有條件的禁用提交按鈕 | ? | ? |
強制輸入格式 | ? | ? |
一個數據的多個輸入 | ? | ? |
動態輸入 | ? | ? |
實際應用場景
場景1
頁面有一些信息和一個編輯按鈕,點擊按鈕出來一個彈窗,彈窗的內容是對信息的更改。
// 我們使用 Redux 來進行狀態管理,數據來源為 props
const Information = props => {
const [name, setName] = useState('');
const [age, setAge] = useState(18);
const nameInputElement = useRef(null);
const ageInputElement = useRef(null);
useEffect(() => {
setName(props.name);
setAge(props.age);
}, [props]);
return (
<div>
<div>
<div>姓名:{name}</div>
<div>年齡:{age}</div>
</div>
<Modal>
<div>
<label>
姓名:
{/* 方式1: 受控組件 */}
<input type='text' value={name} onChange={e => setName(e.target.value)}/>
{/* 方式2: 非受控組件 */}
<input type='text' defaultValue={name} ref={nameInputElement}/>
{/* 方式3: 非受控組件 */}
<input type='text' defaultValue={name} onChange={e => setName(e.target.value)}/>
{/* 方式4: 非受控組件 */}
<input type='text' defaultValue={props.name} onChange={e => setName(e.target.value)}/>
</label>
</div>
<div>
<label>
年齡:
{/* 方式1: 受控組件 */}
<input type='text' value={age} onChange={e => setAge(Number(e.target.value))}/>
{/* 方式2: 非受控組件 */}
<input type='text' defaultValue={age} ref={nameInputElement} />
{/* 方式3: 非受控組件 */}
<input type='text' defaultValue={age} onChange={e => setAge(Number(e.target.value))}/>
{/* 方式4: 非受控組件 */}
<input type='text' defaultValue={props.age} onChange={e => setAge(Number(e.target.value))}/>
</label>
</div>
</Modal>
</div>
)
};
上面有4種方式:請注意:這四種方式都是在 <Modal>
中使用的
- 方式1:在
<Modal>
中使用方式1的時候,彈窗出來之后對內容進行修改,之后點擊取消關閉彈窗后,再點擊編輯按鈕,此時彈窗內容還是修改后的內容。所以如果使用方式1:受控組件來進行<Modal>
時,需要在closeModal
的方法中對內容進行重置。 - 方式2:這種方式使用
ref
獲取表單的值,但是有一個問題就是,獲取用戶的age
是異步的,當拿到用戶的age
后,頁面重新render
,但是此方式不會進行age
的更新,故不使用此方式。 - 方式3:這種方式使用
onChange
獲取表單的值,但是這種方式和方式2有同樣的問題,故不使用此方式。 - 方式4:這種方式彈窗出來之后對內容的進行修改,之后點擊取消關閉彈窗,再點擊編輯按鈕,內容是原有的內容,并且關閉時不需要重置。
注:方式2與方式3的區別在于方式3會 render
,而方式2不會 render
總結:方式1(受控組件)與方式4(非受控組件)都可以進行使用,方式1 closeModal
時需要重置修改內容。
場景2
頁面中有一個單選框(使用 ant Design 的 <Radio>
) + 一個保存按鈕,單選框選擇性別,保存按鈕可以對性別進行保存
const Page = props => {
const [sex, setSex] = setState('');
const sexElement = useRef(null);
useEffect(() => {
setSex(props.sex);
}, [props]);
const save = () => {
console.log('你的性別 -> ', sex);
};
return (
<div>
性別:
{/* 方式1: 受控組件 */}
<Radio.Group value={sex} onChange={e => setSex(e.target.value)}>
<Radio.Button value='male'>男</Radio.Button>
<Radio.Button value='female'>女</Radio.Button>
</Radio.Group>
{/* 方式2: 非受控組件 */}
<Radio.Group defaultValue={sex} ref={sexElement}>
<Radio.Button value='male'>男</Radio.Button>
<Radio.Button value='female'>女</Radio.Button>
</Radio.Group>
{/* 方式3: 非受控組件 */}
<Radio.Group defaultValue={sex} onChange={e => setSex(e.target.value)}>
<Radio.Button value='male'>男</Radio.Button>
<Radio.Button value='female'>女</Radio.Button>
</Radio.Group>
{/* 方式4: 非受控組件 */}
<Radio.Group defaultValue={props.sex} onChange={e => setSex(e.target.value)}>
<Radio.Button value='male'>男</Radio.Button>
<Radio.Button value='female'>女</Radio.Button>
</Radio.Group>
<button onClick={save}>保存</button>
</div>
)
};
上面有4種方式:請注意:這四種方式都是在頁面中使用的
- 方式1:受控組件,沒有任何問題
- 方式2:異步獲取
sex
之后頁面沒有進行更改,故不使用此方式。 - 方式3:和方式2相同的問題,故不使用此方式。
- 方式4:實踐下來之后和方式2具有相同的問題
總結:在頁面中使用的時候乖乖用受控組件。
總結
我遇到了場景2中方式4的問題,所以我谷歌了一下,答案就是去研究受控組件,所有就誕生了這篇文章。另外千萬不要在受控組件中使用 debounce
,因為受控組件的一個特點就是實時。在非受控組件中,個人感覺 debounce
還是有必要使用一下的,從而優化了一下 render
的次數。