我无法在条件渲染上正确使用 useEffect 读取输入

I can't make useEffect read input properly on conditional render

我正在尝试创建具有 'static' 模式的输入组件。在这种模式下,输入被替换为包含输入值的范围。在这个问题中,我使用了范围输入。

import React from 'react';
import ReactDOM from 'react-dom';

const { useState, useEffect, } = React;

const RangeInput = ({
  inputID, min, max, defaultValue, children,
}) => {
  const [valueAfterStaticChange, setValueAfterStaticChange] = useState(
    defaultValue
  );
  const [isStatic, setIsStatic] = useState(false);
  useEffect(
    () => () => {
      // do this before component unmounts and when 'isStatic' changes,
      // but only when 'isStatic' is true
      isStatic
        || setValueAfterStaticChange(document.getElementById(inputID).value);
    },
    [isStatic]
  );
  return (
    <div>
      <label htmlFor={inputID}>{children}</label>
      <span style={{ display: isStatic || 'none', }}>
        {valueAfterStaticChange}
      </span>
      <input
        type="range"
        min={min}
        max={max}
        defaultValue={valueAfterStaticChange}
        id={inputID}
        style={{ display: isStatic && 'none', }}
      />
      <button
        onClick={() => {
          setIsStatic(!isStatic);
        }}
      >
        Toggle Static
      </button>
    </div>
  );
};

ReactDOM.render(
  <RangeInput inputID="myID" min={0} max={100} defaultValue={50}>
    My Range Input
  </RangeInput>,
  document.getElementById('root')
);

以上代码有效。但是它用display: none来模拟一个'no render'。我认为我可以使用以下逻辑完成实际卸载:

// all unchanged until... 

  return (
    <div>
      <label htmlFor={inputID}>{children}</label>
      {isStatic ? (
        <span>{valueAfterStaticChange}</span>
      ) : (
        <input
          type="range"
          min={min}
          max={max}
          defaultValue={valueAfterStaticChange}
          id={inputID}
        />
      )}
      <button
        onClick={() => {
          setIsStatic(!isStatic);
        }}
      >
        Toggle Static
      </button>
    </div>
  );
};

我的 useEffect return 函数在实际卸载后无法正确读取输入。我认为在 useEffect 中使用 return 函数会导致在组件卸载之前 运行 的函数。我做错了什么?

如果未安装 <input> 元素,您自然无法访问 document.getElementById(inputID).value - 它不存在!即便如此,您也不会希望以这种方式读取其值。这是一种非常 un-React 的做事方式。 React 组件应该在 state 和 props 中拥有他们需要的一切——如果你发现自己直接查询或操作 DOM,你(几乎总是)走在黑暗的道路上。

相反,您想要的是 controlled component

受控组件不依赖 DOM 来存储 "value" 之类的任何内容。相反,该组件仅显示 React 在状态中具有的任何值并提供方便的事件挂钩。

我强烈建议您阅读我链接的文档,看看您是否可以自己想出一个实现。这毕竟是最好的学习方式!但是,如果您仍然无法弄清楚,我会回来并用一个基本的工作示例来编辑我的答案。

编辑:这是一个带有一些注释的简单工作演示。

https://stackblitz.com/edit/controlled-input-demo?file=index.js

注意其实很简单! <App> 跟踪状态中的滑块值,而 <RangeInput> 只是将该值作为道具使用并提供事件处理程序以将其更新回 <App><RangeSlider> 可以跟踪它自己的 "editing mode" 值,因为其他组件可能不需要访问它(但如果你愿意,你可以 "lift" 声明一个级别!)你甚至不需要useEffect()这里!

除非有理由在卸载时获取值,否则您似乎可以在值发生变化时跟踪它。这样你就根本不需要 useEffect:

const RangeInput = ({ inputID, min, max, defaultValue, children }) => {
  const [value, setValue] = useState(defaultValue); // <-- keep track of value
  const [isStatic, setIsStatic] = useState(false);

  return (
    <div>
      <label htmlFor={inputID}>{children}</label>
      {isStatic ? (
        <span>{value}</span>
      ) : (
        <input
          type="range"
          min={min}
          max={max}
          defaultValue={value}
          onChange={e => setValue(e.target.value)} // <-- Set value onChange
          id={inputID}
        />
      )}
      <button
        onClick={() => {
          setIsStatic(!isStatic);
        }}
      >
        Toggle Static
      </button>
    </div>
  );
};