React:结合 useReducer 和 useState

时间:2021-02-17 03:39:06

标签: javascript reactjs forms use-state use-reducer

我正在处理的组件是表单的时间输入。表单相对复杂,是动态生成的,根据嵌套在其他数据中的数据显示不同的字段。我正在使用 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,也许这是一个提示?

谢谢大家!!

1 个答案:

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

Edit react-managing-form-state-with-a-combination-of-usereducer-and-usestate