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 应用示例。
每个 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 应用示例。