React:结合使用 useReducer 和 useState 管理表单状态
React: Managing form state with a combination of useReducer and useState
我正在处理的组件是表单的时间输入。表单相对复杂并且是动态生成的,根据嵌套在其他数据中的数据出现不同的字段。我正在使用 useReducer 管理表单的状态,到目前为止效果很好。现在我正在尝试实现一个时间输入组件,我想进行一些基本的验证,特别是这样我就不会将无格式的垃圾数据输入到我的数据库中。我的想法是我的数据库需要一件事:一个时间,按照 ISO8601 格式化。另一方面,UI 可以通过多种方式获取该日期,在我的例子中是通过“小时”字段、“分钟”字段,最后是 am/pm 字段。由于多个字段被单独验证,然后组合成一个 ISO 字符串,我的方法是让 useState 管理各个字段及其验证,然后将一个处理过的 ISO 字符串分派到我的集中状态。
为了让它工作,我尝试让输入字段的 onChange 侦听器简单地使用经过验证的输入更新本地状态,然后让 useEffect 使用其依赖项数组“侦听”本地状态。因此,每次本地状态发生变化时,useEffect 回调都会在其有效负载中使用新输入分派一个操作,该输入现已处理为 ISO 字符串。我有点惊讶这能奏效,但我还有很多东西要学……所有这些。无论如何,这很有效,或者我认为..
由于所讨论的组件 TimePiece 正在其父组件的父组件内动态呈现(在嵌套循环内),当用户稍微更改表单时,TimePiece 组件将使用新的属性和状态呈现。但问题就在这里,每次渲染 TimePiece 时,它都与 TimePiece 的每个其他“实例”具有相同的状态(尽管它是一个函数组件)。我使用了一些 console.logs 来发现它实际上保持它的独立状态直到渲染的那一刻,然后它被设置为最后一个被修改的“实例”的状态。
我的中央 useReducer 状态由一系列 id 键控,因此它能够在用户更改视图时持续存在而不会出现类似问题。只是本地状态的行为不正常,在重新渲染的某个地方,它将该状态发送到中央 useReducer 状态并覆盖现有的正确值...
肯定有问题,但我一直在尝试不同的版本,只是把它弄坏了。在某一时刻,它实际上在两种状态之间不断地飘动……我想我会咨询互联网。我这样做完全错了吗?这是一些轻微的调整吗?我不应该在具有本地状态依赖性的 useEffect 内部进行调度吗?
特别是,结合 useState 和 useReducer 是不是很奇怪,无论是广泛地还是以我做过的特定方式?
这是代码..如果它一点意义都没有,我可以制作一个模拟版本,但问题往往出在细节上,所以我想看看是否有人有任何想法。非常感谢。
函数 validateHours 和 validateMinutes 应该不会对操作产生太大影响,如果你想忽略它们(或者我认为......)。
“Mark”是字段状态在内存中的名称,例如ISO 字符串。
io 就是我所说的用户输入。
function TimePiece({ mark, phormId, facetParentId, pieceType, dispatch, markType, recordId }) {
const [hourField, setHourField] = useState(parseIsoToFields(mark).hour);
const [minuteField, setMinuteField] = useState(parseIsoToFields(mark).minute);
function parseFieldsToIso(hour, minute) {
const isoTime = DateTime.fromObject({ hour: hour ? hour : '0', minute: minute ? minute : '0' });
return isoTime.toISOTime();
}
function parseIsoToFields(isoTime) {
const time = DateTime.fromISO(isoTime);
const hour = makeTwoDigit(`${time.hour}`);
const minute = makeTwoDigit(`${time.minute}`);
return {
hour: hour ? hour : '',
minute: minute ? minute : ''
}
}
function makeTwoDigit(value) {
const twoDigit = value.length === 2 ? value :
value.length === 1 ? '0' + value : '00'
return twoDigit;
}
function validateHours(io) {
const isANumber = /\d/g;
const is01or2 = /[0-2]/g;
if (isANumber.test(io) || io === '') {
if (io.length < 2) {
setHourField(io)
} else if (io.length === 2) {
if (io[0] === '0') {
setHourField(io);
} else if ( io[0] === '1' && is01or2.test(io[1]) ) {
setHourField(io);
} else {
console.log('Invalid number, too large..');
}
}
} else {
console.log('Invalid characeter..');
}
}
function validateMinutes(io) {
const isANumber = /\d/g;
const is0thru5 = /[0-5]/;
if (isANumber.test(io) || io === '') {
if (io.length < 2) {
setMinuteField(io);
} else if (is0thru5.test(io[0])) {
setMinuteField(io);
} else {
console.log('Invalid number, too large..');
}
} else {
console.log('Invalid character..');
}
}
useEffect(() => {
dispatch({
type: `${markType}/io`,
payload: {
phormId,
facetId: facetParentId,
pieceType,
io: parseFieldsToIso(hourField, minuteField),
recordId
}
})
}, [hourField, minuteField, dispatch, phormId, facetParentId, pieceType, markType, recordId])
return (
<React.Fragment>
<input
maxLength='2'
value={hourField} onChange={(e) => {validateHours(e.target.value)}}
style={{ width: '2ch' }}
></input>
<span>:</span>
<input
maxLength='2'
value={minuteField}
onChange={(e) => { validateMinutes(e.target.value) }}
style={{ width: '2ch' }}
></input>
</React.Fragment>
)
}
P.S。我制作了另一个版本,它避免使用 useState 而是依赖一个函数来验证和处理字段,但出于某种原因它看起来很奇怪,即使它更实用。此外,拥有本地状态似乎是实现突出显示错误输入并显示“无效数字”或其他内容的理想选择,而不是简单地禁止该输入。
编辑:
现场代码在这里:
https://codesandbox.io/s/gv-timepiecedemo-gmkmp?file=/src/components/TimePiece.js
TimePiece 是 Facet 的子项,Facet 是 Phorm 或 LogPhorm 的子项,后者是 Recorder 或 Log 的子项...希望它有点清晰。
按照建议,我设法让它在 Codesandbox 上运行。我是 运行 一个本地节点服务器,用于路由到 Mongo 数据库,但不知道如何设置,所以我只是用一个虚拟数据库插入它,应该不会影响手头的问题.
要创建问题,请在左上角的下拉菜单中选择“全局库”,然后单击“上拉”或“上推”。然后在主 window 中,尝试在“时间”字段中键入内容。 “Pull-Up”和“Push-Up”都使用了这个TimePiece组件,当你点击另一个时,你会看到那里的Time字段已经变成和其他Time字段一样了。当您在练习之间切换时,其他字段(“Reps”、“Load”)各自保持独立状态,这就是我想要的。
如果您单击“生成记录”并在“时间”字段中设置一些值,则会创建一条“记录”,该记录现在将显示在右侧。如果您单击它,它会展开成与主 window 类似的显示。同样的问题发生在“时间”字段上,除了状态独立于主 window 中的状态。所以基本上有两种状态:一种是主window中的所有时间字段,一种是右侧window中的所有时间字段。这些分别由不同的父级 Phorm 和 LogPhorm 渲染,也许这是一个提示?
谢谢大家!!
好吧,在花了几个小时试图追踪数据流从 TimePiece
返回到所有抽象到“状态”,然后返回,我能真正说的是你有一个吨螺旋桨钻井。几乎所有组件都使用相同或非常相似的 props
我最终发现 TimePiece
在我猜你调用的 Phorms(??) 之间切换时不会卸载,你通过 Widget
抽象了它。一旦我发现 不是 unmounting/remounting 因为我希望显示不同的小时和分钟状态,解决方案很简单:添加一个对应于 Phorm 的 React 键,当你在引体向上和俯卧撑之间切换。
Phorm.js
<Widget
key={phormId} // <-- add react key here
mark={marks[facetParentId][piece.pieceType]}
phormId={phormId}
facetParentId={facetParentId}
dispatch={dispatch}
pieceType={piece.pieceType}
markType={markType}
recordId={recordId}
/>
在此处使用反应键会强制 React 将两个练习小部件时间片段视为两个单独的“实例”,当您在两者之间切换时,组件会重新安装并重新计算 TimePiece
中的初始组件状态。
我正在处理的组件是表单的时间输入。表单相对复杂并且是动态生成的,根据嵌套在其他数据中的数据出现不同的字段。我正在使用 useReducer 管理表单的状态,到目前为止效果很好。现在我正在尝试实现一个时间输入组件,我想进行一些基本的验证,特别是这样我就不会将无格式的垃圾数据输入到我的数据库中。我的想法是我的数据库需要一件事:一个时间,按照 ISO8601 格式化。另一方面,UI 可以通过多种方式获取该日期,在我的例子中是通过“小时”字段、“分钟”字段,最后是 am/pm 字段。由于多个字段被单独验证,然后组合成一个 ISO 字符串,我的方法是让 useState 管理各个字段及其验证,然后将一个处理过的 ISO 字符串分派到我的集中状态。
为了让它工作,我尝试让输入字段的 onChange 侦听器简单地使用经过验证的输入更新本地状态,然后让 useEffect 使用其依赖项数组“侦听”本地状态。因此,每次本地状态发生变化时,useEffect 回调都会在其有效负载中使用新输入分派一个操作,该输入现已处理为 ISO 字符串。我有点惊讶这能奏效,但我还有很多东西要学……所有这些。无论如何,这很有效,或者我认为..
由于所讨论的组件 TimePiece 正在其父组件的父组件内动态呈现(在嵌套循环内),当用户稍微更改表单时,TimePiece 组件将使用新的属性和状态呈现。但问题就在这里,每次渲染 TimePiece 时,它都与 TimePiece 的每个其他“实例”具有相同的状态(尽管它是一个函数组件)。我使用了一些 console.logs 来发现它实际上保持它的独立状态直到渲染的那一刻,然后它被设置为最后一个被修改的“实例”的状态。
我的中央 useReducer 状态由一系列 id 键控,因此它能够在用户更改视图时持续存在而不会出现类似问题。只是本地状态的行为不正常,在重新渲染的某个地方,它将该状态发送到中央 useReducer 状态并覆盖现有的正确值...
肯定有问题,但我一直在尝试不同的版本,只是把它弄坏了。在某一时刻,它实际上在两种状态之间不断地飘动……我想我会咨询互联网。我这样做完全错了吗?这是一些轻微的调整吗?我不应该在具有本地状态依赖性的 useEffect 内部进行调度吗?
特别是,结合 useState 和 useReducer 是不是很奇怪,无论是广泛地还是以我做过的特定方式?
这是代码..如果它一点意义都没有,我可以制作一个模拟版本,但问题往往出在细节上,所以我想看看是否有人有任何想法。非常感谢。
函数 validateHours 和 validateMinutes 应该不会对操作产生太大影响,如果你想忽略它们(或者我认为......)。
“Mark”是字段状态在内存中的名称,例如ISO 字符串。 io 就是我所说的用户输入。
function TimePiece({ mark, phormId, facetParentId, pieceType, dispatch, markType, recordId }) {
const [hourField, setHourField] = useState(parseIsoToFields(mark).hour);
const [minuteField, setMinuteField] = useState(parseIsoToFields(mark).minute);
function parseFieldsToIso(hour, minute) {
const isoTime = DateTime.fromObject({ hour: hour ? hour : '0', minute: minute ? minute : '0' });
return isoTime.toISOTime();
}
function parseIsoToFields(isoTime) {
const time = DateTime.fromISO(isoTime);
const hour = makeTwoDigit(`${time.hour}`);
const minute = makeTwoDigit(`${time.minute}`);
return {
hour: hour ? hour : '',
minute: minute ? minute : ''
}
}
function makeTwoDigit(value) {
const twoDigit = value.length === 2 ? value :
value.length === 1 ? '0' + value : '00'
return twoDigit;
}
function validateHours(io) {
const isANumber = /\d/g;
const is01or2 = /[0-2]/g;
if (isANumber.test(io) || io === '') {
if (io.length < 2) {
setHourField(io)
} else if (io.length === 2) {
if (io[0] === '0') {
setHourField(io);
} else if ( io[0] === '1' && is01or2.test(io[1]) ) {
setHourField(io);
} else {
console.log('Invalid number, too large..');
}
}
} else {
console.log('Invalid characeter..');
}
}
function validateMinutes(io) {
const isANumber = /\d/g;
const is0thru5 = /[0-5]/;
if (isANumber.test(io) || io === '') {
if (io.length < 2) {
setMinuteField(io);
} else if (is0thru5.test(io[0])) {
setMinuteField(io);
} else {
console.log('Invalid number, too large..');
}
} else {
console.log('Invalid character..');
}
}
useEffect(() => {
dispatch({
type: `${markType}/io`,
payload: {
phormId,
facetId: facetParentId,
pieceType,
io: parseFieldsToIso(hourField, minuteField),
recordId
}
})
}, [hourField, minuteField, dispatch, phormId, facetParentId, pieceType, markType, recordId])
return (
<React.Fragment>
<input
maxLength='2'
value={hourField} onChange={(e) => {validateHours(e.target.value)}}
style={{ width: '2ch' }}
></input>
<span>:</span>
<input
maxLength='2'
value={minuteField}
onChange={(e) => { validateMinutes(e.target.value) }}
style={{ width: '2ch' }}
></input>
</React.Fragment>
)
}
P.S。我制作了另一个版本,它避免使用 useState 而是依赖一个函数来验证和处理字段,但出于某种原因它看起来很奇怪,即使它更实用。此外,拥有本地状态似乎是实现突出显示错误输入并显示“无效数字”或其他内容的理想选择,而不是简单地禁止该输入。
编辑: 现场代码在这里: https://codesandbox.io/s/gv-timepiecedemo-gmkmp?file=/src/components/TimePiece.js
TimePiece 是 Facet 的子项,Facet 是 Phorm 或 LogPhorm 的子项,后者是 Recorder 或 Log 的子项...希望它有点清晰。
按照建议,我设法让它在 Codesandbox 上运行。我是 运行 一个本地节点服务器,用于路由到 Mongo 数据库,但不知道如何设置,所以我只是用一个虚拟数据库插入它,应该不会影响手头的问题.
要创建问题,请在左上角的下拉菜单中选择“全局库”,然后单击“上拉”或“上推”。然后在主 window 中,尝试在“时间”字段中键入内容。 “Pull-Up”和“Push-Up”都使用了这个TimePiece组件,当你点击另一个时,你会看到那里的Time字段已经变成和其他Time字段一样了。当您在练习之间切换时,其他字段(“Reps”、“Load”)各自保持独立状态,这就是我想要的。
如果您单击“生成记录”并在“时间”字段中设置一些值,则会创建一条“记录”,该记录现在将显示在右侧。如果您单击它,它会展开成与主 window 类似的显示。同样的问题发生在“时间”字段上,除了状态独立于主 window 中的状态。所以基本上有两种状态:一种是主window中的所有时间字段,一种是右侧window中的所有时间字段。这些分别由不同的父级 Phorm 和 LogPhorm 渲染,也许这是一个提示?
谢谢大家!!
好吧,在花了几个小时试图追踪数据流从 TimePiece
返回到所有抽象到“状态”,然后返回,我能真正说的是你有一个吨螺旋桨钻井。几乎所有组件都使用相同或非常相似的 props
我最终发现 TimePiece
在我猜你调用的 Phorms(??) 之间切换时不会卸载,你通过 Widget
抽象了它。一旦我发现 不是 unmounting/remounting 因为我希望显示不同的小时和分钟状态,解决方案很简单:添加一个对应于 Phorm 的 React 键,当你在引体向上和俯卧撑之间切换。
Phorm.js
<Widget
key={phormId} // <-- add react key here
mark={marks[facetParentId][piece.pieceType]}
phormId={phormId}
facetParentId={facetParentId}
dispatch={dispatch}
pieceType={piece.pieceType}
markType={markType}
recordId={recordId}
/>
在此处使用反应键会强制 React 将两个练习小部件时间片段视为两个单独的“实例”,当您在两者之间切换时,组件会重新安装并重新计算 TimePiece
中的初始组件状态。