Skip to content
Advertisement

SetState inside useEffect is causing side effects on select input functionality

Each select menu comes with a help text inside a box. Similar to a tooltip. User can close them when clicking ‘close button’ or clicking outside.

My solution works and they are being closed each time you click outside them.

The problem is that setState inside the useEffect has a side effect on the select menus.

The issue is when I close the info box using the ‘close button’ or click inside the info box. After I close it with the button or click inside it, if I try to change an option, I see the options flickering and I can’t change selection, it would only work the second time.

Here is my code: 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>
  );
}

Advertisement

Answer

The flickering happens because you have one huge component that re-renders each time you toggle the visibility of the info text. As soon as you click on the select, the whole component gets re-rendered which leads to the select being closed right away.

To solve this, you have to prevent the whole component from re-rendering. Separate it into smaller chunks, which can be rerendered separately. Here is a simplified example to show how to isolate the info section into a self-managed component.

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>
  );
}

Don’t forget to remove your event listener, as soon as you don’t need them anymore, e.g. when the component gets unmounted:

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

Otherwise, this could lead to bugs and performance issues.

Here is your stackblitz with the applied example.

User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement