解决问题: 🌿 分散的 state,导致代码扩展&维护困难; 🌾 对于输入值的控制/转换等(如希望限制age在1-120之间)
React 表单场景的开发中,往往需要维护众多 state (如,表单数据),过多的 state 会导致源代码冗长,可读性比较差;且未来增删改字段,需要修改的地方也较多,难以维护。
举例:下述表单有三个字段,需要提交给服务
针对每个字段封装单独的 state 管理。
export default () => {
const [name, setName] = useState('');
const [age, setAge] = useState(null);
const [address, setAddress] = useState('');
return (
<div>
<label htmlFor="name">name</label>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} /><br/>
<label htmlFor="age">age</label>
<input type="number" value={age} onChange={(e) => setAge(e.target.value)} /><br/>
<label htmlFor="address">address</label>
<input type="text" value={address} onChange={(e) => setAddress(e.target.value)} /><br/>
{name}-{age}-{address}
</div>
)
}
每个表单项有自己的 state 及对应的修改值的方法,如果未来对某个表单项进行增删改,与 state 配套的 DOM 需要同步处理。
将字段封装到一个 state 管理。
export default () => {
const [personalInfo, setPersonalInfo] = useState({
name: '',
age: null,
address: ''
})
return (
<div>
{
Object.keys(personalInfo).map((key) => {
return <div key={key}>
<label htmlFor={key}>{key}</label>
<input type="text" value={personalInfo[key]} onChange={(e) => setPersonalInfo({...personalInfo, [key]: e.target.value})} />
</div>
})
}
{personalInfo.name}-{personalInfo.age}-{personalInfo.address}
</div>
)
}
这种方式可以精简代码,但需要注意不能直接改变原对象,需要通过 ==...personalInfo
来处理。
如果需要对某个值从“数据”层面(如age只允许1-120)做判断,使用这种方式无法完成。
当然,首先要在UI中提供验证
使用 reducer 进行封装管理。如果对 reducer 还不熟悉,可以跳转到文章尾部,查看相关介绍(来自官网)。
export default () => {
const [personalInfo, setPersonalInfo] = useReducer((state, next) => {
return {...state, ...next};
}, {name: '', age: null, address: ''})
return (
<div>
{
Object.keys(personalInfo).map((key) => {
return <div key={key}>
<label htmlFor={key}>{key}</label>
<input type="text" value={personalInfo[key]} onChange={(e) => setPersonalInfo({[key]: e.target.value})} />
</div>
})
}
{personalInfo.name}-{personalInfo.age}-{personalInfo.address}
</div>
)
}
以一种集中的方式且可以确保 state 总是有效的。并提供了一个控制 state 的函数能力(可以控制无效的数据,避免无效的渲染)。
如:上述提到的,希望age控制在1-120之间
const [personalInfo, setPersonalInfo] = useReducer((state, next) => {
const newState = {...state, ...next};
if (newState.age > 120) newState.age = 120;
if (newState.age < 1) newState.age = 1;
return newState;
}, {name: '', age: null, address: ''})
当然,这里也可以使用官方推荐的方式:
const [personalInfo, setPersonalInfo] = useReducer((state, action) => {
const newState = {...state};
switch (action.type) {
case 'updateAge': {
newState.age = action.age;
if (newState.age > 120) newState.age = 120;
if (newState.age < 1) newState.age = 1;
break;
}
}
return newState;
}, {name: '', age: null, address: ''})
其调用 onChange={(e) => setPersonalInfo({type: 'updateAge', age: e.target.value})}
‼️ useReducer 的状态值(state)是不可变的,不能更改!
useReducer((state, next) => {
// ❌ 改变现存 state 对象
state.age = next.age;
return state
// ✔️ 创建新的对象
return {...state, ...next};
}, {name: '', age: null, address: ''})
这个问题可以通过 Immer 解决。
对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。 对于这种情况,可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。
useReducer
是一个 React Hook,允许向组件里面添加一个 reducer。
const [state, dispatch] = useReducer(reducer, initialArg, init?)
参数:
reducer
:用于更新 state 的纯函数。参数为 state 和 action,返回值是更新后的 state。state 与 action 可以是任意合法值。initialArg
:用于初始化 state 的任意值。初始值的计算逻辑取决于接下来的 init
参数。init
:用于计算初始值的函数。如果存在,使用 init(initialArg)
的执行结果作为初始值,否则使用 initialArg
。返回值:
state
: 初次渲染时,它是 init(initialArg)
或 initialArg
(如果没有 init
函数)。dispatch
函数:用于更新 state 并触发组件的重新渲染。function myReducer (state, action) {
// 给 React 返回更新后的状态
return {...}
}
state
)作为第一个参数;action
对象作为第二个参数;reducer
返回 下一个 状态(React 会将旧的状态设置为这个最新的状态「返回值 state
」)。dispatch
函数允许更新 state 并触发组件的重新渲染。它需要传入一个 action 作为参数:
dispatch({ type: 'incremented_age' });
可以是任意类型的值。通常来说 action 是一个对象,其中 type
属性标识类型,其它属性携带额外信息。