概述
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
的次数。