我正在处理的组件是表单的时间输入。表单相对复杂,是动态生成的,根据嵌套在其他数据中的数据显示不同的字段。我正在使用 useReducer 管理表单的状态,到目前为止它运行良好。现在我正在尝试实现一个时间输入组件,我想进行一些基本的验证,特别是这样我就不会将无格式的垃圾数据放入我的数据库中。我的想法是我的数据库想要一件事:一个时间,按照 ISO8601 格式化。另一方面,用户界面可以通过多种方式获取该日期,在我的例子中是通过“小时”字段、“分钟”字段,最终是上午/下午字段。由于多个字段被单独验证,然后组合成单个 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>
)
}
附言我制作了另一个版本,它避免使用 useState 而是依靠一个函数来验证和处理字段,但由于某种原因,它看起来很奇怪,即使它更实用。此外,拥有本地状态似乎是实现突出显示错误输入并显示“无效数字”或其他内容的理想选择,而不是简单地禁止该输入。
编辑: 现场代码在这里: https://codesandbox.io/s/gv-timepiecedemo-gmkmp?file=/src/components/TimePiece.js
TimePiece 是 Facet 的子代,后者是 Phorm 或 LogPhorm 的子代,后者是 Recorder 或 Log 的子代...希望它有点清晰。
按照建议,我设法让它在 Codesandbox 上运行。我正在运行一个本地节点服务器来路由到 Mongo 数据库,但不知道如何设置它,所以我只是将它插入了一个虚拟数据库,应该不会影响手头的问题。
要创建问题,请在左上角的下拉菜单中选择“全局库”,然后单击“上拉”或“上推”。然后在主窗口中,尝试在“时间”字段中输入。 “Pull-Up”和“Push-Up”都使用了这个 TimePiece 组件,当您点击另一个时,您会看到那里的时间字段已更改为与其他时间字段相同。当你在练习之间切换时,其他字段(“Reps”、“Load”)每个都保持自己独立的状态,这就是我想要的。
如果您在“时间”字段中单击带有某些值的“生成记录”,则会生成一个“记录”,现在将显示在右侧。如果您单击它,它会扩展为与主窗口类似的显示。同样的问题在这里发生在“时间”字段中,除了状态独立于主窗口中的状态。所以基本上有两种状态:一种用于主窗口中的所有时间字段,一种用于右侧窗口中的所有时间字段。那些是由不同的父母分别渲染的,Phorm 和 LogPhorm,也许这是一个提示?
谢谢大家!!
答案 0 :(得分:1)
好的,在花了几个小时试图追踪从 TimePiece
到所有抽象到“状态”的数据流之后,我只能说你有很多支柱钻孔。几乎所有组件都使用相同或非常相似的 props
我最终发现 TimePiece
在我猜你称之为 Phorms(??) 的东西之间切换时不会卸载,你已经通过 Widget
抽象了它。一旦我发现什么不是卸载/重新安装,因为我希望显示不同的小时和分钟状态,解决方案很简单:当你在上拉之间切换时,添加一个对应于 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 键会强制 React 将两个练习小部件时间段视为两个单独的“实例”,当您在两者之间切换时,组件会重新安装并重新计算 TimePiece
中的初始组件状态。