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.