useEffect 中的 SetState 对 select 输入功能造成副作用

SetState inside useEffect is causing side effects on select input functionality

每个 select 菜单都带有一个框内的帮助文本。类似于工具提示。用户可以在单击 'close button' 或单击外部时关闭它们。

我的解决方案有效,每次在它们外面单击时它们都会关闭。

问题是 useEffect 中的 setState 对 select 菜单有副作用。

问题是当我使用 'close button' 关闭信息框或在信息框内单击时。在我用按钮关闭它或在其中单击后,如果我尝试更改一个选项,我会看到选项在闪烁并且我无法更改 selection,它只能在第二次工作。

这是我的代码:https://stackblitz.com/edit/react-61rzle?file=src%2FSelect.js

export default function Select() {
  const selectMenus = [
    {
      Label: 'Select 1',
      Name: 'select1',
      DefaultValue: '1',
      HelpText: 'Help text',
      Id: 'select_1',
      Options: [
        {
          Value: '0',
          Text: 'All age groups',
        },
        {
          Value: '1',
          Text: 'Less than 35',
        },
        {
          Value: '2',
          Text: '35 - 37 yrs',
        },
        {
          Value: '3',
          Text: '38 - 39 yrs',
        },
        {
          Value: '4',
          Text: '40 - 42 yrs',
        },
        {
          Value: '5',
          Text: '43 - 44 yrs',
        },
        {
          Value: '6',
          Text: '45 yrs +',
        },
      ],
    },
    {
      Label: 'Select 2',
      Name: 'select2',
      DefaultValue: '0',
      HelpText: 'Help text',
      Id: 'select_2',
      Options: [
        {
          Value: '0',
          Text: 'All',
        },
        {
          Value: '1',
          Text: 'Less than 35',
        },
        {
          Value: '2',
          Text: '43 - 44 yrs',
        },
      ],
    },
  ];
  const [value, setValue] = useState({
    select1: '',
    select2: '',
  });

  // help texts setup
  const initialVisibleHelpTexts = {
    info0: false,
    info1: false,
    info2: false,
  };

  const [visibleHelpText, setVisibleHelpText] = useState(
    initialVisibleHelpTexts
  );

  const showHelpText = (e, key) => {
    e.preventDefault();
    e.stopPropagation();
    setVisibleHelpText({ ...initialVisibleHelpTexts, ...{ [key]: true } });
  };

  const hideHelpText = (e, key) => {
    e.preventDefault();
    e.stopPropagation();
    setVisibleHelpText({ ...visibleHelpText, ...{ [key]: false } });
  };

  // close info on click outside
  useEffect(() => {
    document.addEventListener('click', function (e) {
      e.preventDefault();
      e.stopPropagation();
      if (
        e.target.parentNode.className !== 'info__content' &&
        e.target.parentNode.className !== 'info__content-header-text' &&
        e.target.parentNode.className !== 'info__content-header'
      ) {
        setVisibleHelpText(initialVisibleHelpTexts);
      }
    });
  }, []);

  const handleOnChange = (e) => {
    const valueSelected = e.target.value;
    setValue({
      ...value,
      [e.target.name]: valueSelected,
    });
  };

  return (
    <form>
      {selectMenus.length > 0 && (
        <div className="selectors-container">
          {selectMenus.map((select, i) => (
            <div className="select" key={uuid()}>
              <div className="select__label-container">
                <div className="select__title">
                  <label className="select__label" htmlFor={select.Id}>
                    {select.Label}
                  </label>
                  <button
                    className="select__info"
                    onClick={(e) => {
                      showHelpText(e, `info${i}`);
                    }}
                  >
                    Show info
                  </button>
                </div>
                {visibleHelpText[`info${i}`] && (
                  <div className="info">
                    <div className="info__content">
                      <div className="info__content-header">
                        <span className="info__content-header-title">
                          {select.Label}
                        </span>
                        <button
                          onClick={(e) => {
                            hideHelpText(e, `info${i}`);
                          }}
                        >
                          Close info
                        </button>
                      </div>
                      <div className="info__content-header-text">
                        {select.HelpText}
                      </div>
                    </div>
                  </div>
                )}
              </div>
              <div className="select__menu-btn-container">
                <div className="select__container">
                  <select
                    name={select.Name}
                    id={select.Id}
                    value={value[`${select.Name}`]}
                    onChange={handleOnChange}
                  >
                    {select.Options.map((option) => (
                      <option value={option.Value} key={uuid()}>
                        {option.Text}
                      </option>
                    ))}
                  </select>
                </div>
              </div>
            </div>
          ))}
        </div>
      )}
    </form>
  );
}

闪烁的发生是因为你有一个巨大的组件,每次切换信息文本的可见性时 re-renders。只要您单击 select,整个组件就会变成 re-rendered,这会导致 select 立即关闭。

要解决这个问题,您必须阻止整个组件 re-rendering。将它分成更小的块,可以单独重新渲染。 这是一个简化的示例,展示了如何将信息部分隔离到 self-managed 组件中。

function InfoSection({ select }) {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div className="select__label-container">
      <div className="select__title">
        <label className="select__label" htmlFor={select.Id}>
          {select.Label}
        </label>
        <button
          className="select__info"
          onClick={(e) => {
            setIsVisible(true);
          }}
        >
          Show info
        </button>
      </div>
      {isVisible && <InfoText setIsVisible={setIsVisible} />}
    </div>
  );
}
function InfoText({ setIsVisible }) {
  function handleCLickOutside(e) {
    setIsVisible(false);
  }

  useEffect(() => {
    document.addEventListener('click', handleCLickOutside);
//this will remove the event listener, when the component gets unmounted. This is important!
    return () => document.removeEventListener('click', handleCLickOutside); 
  }, []);
  
  return (
    <div className="info">
      <div className="info__content">
        <div className="info__content-header">
          <span className="info__content-header-title">{'label'}</span>
          <button onClick={console.log}>Close info</button>
        </div>
        <div className="info__content-header-text">{'select.HelpText'}</div>
      </div>
    </div>
  );
}

不要忘记删除您的事件侦听器,一旦您不再需要它们,例如当组件被卸载时:

return () => document.removeEventListener('click', handleCLickOutside); 

否则,这可能会导致错误和性能问题。

Here 是你的 stackblitz 应用示例。