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 中的初始组件状态。