SetState inside useEffect is causing side effects on select input functionality - javascript

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

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.

Related

Unfocusing dropdown menu sets its state as undefined

I'm making a dropdown menu that allows user to set a state and then see the page corresponding to chosen values.
I isolated my code to fully reproduce the issue both in text and in this [CodeSandbox]
Desired baheviour - Open menu, set state using its componets, close menu and keep the state.
Current behaviour - Open menu, set state using its components, close menu and state is set to undefined.
I track the changes to the state in the console and can clearly see that adding items to filter is seen in the updated state every time. However when I close the menu the state changes to undefined and the state is unsuable for my needs.
How do I change the code so the state persists when the menu is closed?
Thanks in advance for your time!
import React from "react";
import { default as ReactSelect } from "react-select";
import { components } from "react-select";
export default function BettingDeck(props) {
const sportsOptions = [
{ value: "soccer", label: "Soccer" },
{ value: "dota", label: "Dota 2" },
{ value: "tennis", label: "Tennis" },
{ value: "csgo", label: "CS:GO" }
];
const Option = (props) => {
return (
<div>
<components.Option {...props}>
<input
type="checkbox"
checked={props.isSelected}
onChange={() => null}
/>{" "}
<label>{props.label}</label>
</components.Option>
</div>
);
};
const [sportsSelectorState, setSportsSelectorState] = React.useState({
optionSelected: [],
isFocused: true
});
function handleChange(selected) {
setSportsSelectorState(() => {
return { optionSelected: selected };
});
}
console.log(sportsSelectorState.optionSelected);
return (
<>
<div className="betting-deck-container">
<div className="betting-deck-head-container">
<div className="betting-deck-title">Betting Deck</div>
{/* <SportsSelector /> */}
<span
class="d-inline-block"
data-toggle="popover"
data-trigger="focus"
data-content="Please selecet account(s)"
onBlur={() => {
setSportsSelectorState({ isFocused: false });
}}
onFocus={() => {
setSportsSelectorState({ isFocused: true });
}}
style={
sportsSelectorState.isFocused ? { zIndex: 1 } : { zIndex: 0 }
}
>
<ReactSelect
options={sportsOptions}
isMulti
closeMenuOnSelect={false}
hideSelectedOptions={false}
components={{
Option
}}
onChange={handleChange}
allowSelectAll={true}
value={sportsSelectorState.optionSelected}
placeholder="Select sports to filter"
menuPortalTarget={document.body}
classNamePrefix="mySelect"
/>
</span>
</div>
</div>
</>);}
Every time you set the state, you overwrite it with a new object.
So this:
setSportsSelectorState(() => {
return { optionSelected: selected };
});
practically removes isFocused from the object.
And this removes optionSelected:
setSportsSelectorState({ isFocused: true });
So to always preserve the entire object, spread the previous state (object) into the new and only overwrite the relevant property:
// The parameter in the callback function (prev)
// always holds the previous state, or should
// I say the state as it currently is
// before you change it.
setSportsSelectorState((prev) => {
return { ...prev, isFocused: true };
});
// or
setSportsSelectorState((prev) => {
return { ...prev, optionSelected: selected };
});

Getting content of currently active Text component wrapped inside popover of antd

I am using antd components for my react app. I have a Text component wrapped inside of Popover component. Now in my case this Popover is applied to one particular column of table, i.e. every row-element in that column has a Popover component rendered for it upon mouse hovering.
title: "Name",
dataIndex: "name",
key: "name-key",
sortType: "string",
sortDirections: ["descend", "ascend"],
sorter: (a, b) => a.name.length - b.name.length,
render: (text, record) => (
<Popover>
<Text onMouseOver={handleOnMouseOverCommitId}> {name} </Text>
</Popover>
)
I want to get hold of the row-element's value, the one contained by the above Text component whenever I hover over it. In this case the value denoted by {name} above.
I tried getting it with e.target.value via onMouseOver event, but it returned undefined.
I think I get the reason behind it, because the event.target returns an html node of type <span>.
With a normal div element e.target.value has worked in the past for me. But doing the same thing with a predefined component like antd's Text seems a bit trickier.
Just to elaborate, the Popover has two buttons and based on which button user clicks, I need to render some other components, something like an overlay component.
But in order to do that I would also need to get hold of the text value which originally triggered the Popover.
Below is the code(most of the things removed for preciseness).
record.name is what I ultimately need to capture.
<Popover
content={
<>
<Space>
<Button onClick={showSomeOverlayPaneForName}>
{"View Details for record.name"}
</Button>
<Button href={"https://abc.xyz.com/" + record.role}>
{"View Role Details"}
</Button>
</Space>
</>
}
trigger={"hover"}
>
<Text style={{"color": blue.primary}} copyable={true} onMouseOver={handleOnMouseOverName}>{record.name}</Text>
</Popover>
The handleOnMouseOverName function(which doesn't work anyway) :
const handleOnMouseOverName = (e) => {
//console.log("e.target.value :--- ", e.target.value);
setCurrentActiveName(e.target.value)
}
And once my currentActiveName variable is set(via useState), I use that value inside my function showSomeOverlayPaneForName
const showSomeOverlayPaneForName = (e) => {
axios
.get(
`some-url`,
{
params: {name: currentActiveName}
}
)
.then((response) => {
setData(response.data);
}).catch(reason => {
//xyz
});
}
You need to pass on the record of the enclosing render function to the handleOnMouseOverName function.
Check the following example
import React from 'react';
import 'antd/dist/antd.css';
import './index.css';
import { Space, Table, Button, Popover } from 'antd';
const App = () => {
const data = [
{
key: '1',
name: 'John Brown',
address: 'New York No. 1 Lake Park',
role: 'admin',
},
{
key: '2',
name: 'Jim Green',
address: 'London No. 1 Lake Park',
role: 'user',
},
{
key: '3',
name: 'Joe Black',
address: 'Sidney No. 1 Lake Park',
role: 'manager',
},
];
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
const content = (
<>
<Space>
<Button
onClick={() => {
viewDetail(record);
}}
>
{'View Details for ' + record.name}
</Button>
<Button href={'https://abc.xyz.com/' + record.role}>
{'View Role Details'}
</Button>
</Space>
</>
);
return (
<>
<Popover content={content} title="Details">
<div
onMouseOver={() => {
handleOnMouseOverName(record);
}}
>
{name}
</div>
</Popover>
</>
);
},
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
];
const handleOnMouseOverName = (record) => {
console.log(record);
};
const viewDetail = (record) => {
console.log(record);
};
return <Table columns={columns} dataSource={data} />;
};
export default App;
Output:
I hope this helps.
From antd docs: https://ant.design/components/popover/#header
Apparently you're supposed to render the <Popover/> with a content={content}-prop
For example
const content = <div>Content to render under title</div>
const App = () => {
const value = "Text to hover";
return (
<Popover content={content} title="Title">
<Text>{value}</Text>
</Popover>
)
}

How to have Vue dynamic button disabled based on checkbox from list

I've created a Todo app while learning Vue, so far so good. I have on issue where initially I set the tasks delete button to disabled and want to have it be enabled only if the checkbox related is checked. As of now when I click any task checkbox ALL the buttons are affected. Not sure how you would create a relation to the 2 elements.
<template>
<div class="mt-5 todos-list">
<div
v-for="todo in todos"
:key="todo.id"
class="flex justify-between pb-4 todos-item"
>
<div class="flex items-center mr-10 todos-item__checkbox">
<input
v-model="checkboxes"
:value="todo.task.title"
type="checkbox"
class="w-4 h-4 mr-2 border-2 border-red-500 appearance-none checked:bg-red-600 checked:border-transparent"
#change="checkBoxChecked($event)"
/>
<label for="todo" class="text-lg font-medium todos-checkbox ">{{
todo.task.title
}}</label>
</div>
<div class="todos-item__button">
<button
class="rounded-lg btn btn-delete"
:class="{ disabled: btnDisabled[index] }"
type="button"
:disabled="btnDisabled[index]"
#click="deleteTask(todo.task.id)"
>
Delete
</button>
</div>
<AddTodo #addTodo="addTask" />
</div>
</template>
<script>
export default {
data() {
return {
checkboxes: [],
btnDisabled: [],
todos: [
{
task: {
id: 1,
title: 'Go to store',
isComplete: false
}
},
{
task: {
id: 2,
title: 'Go for walk',
isComplete: false
}
},
{
task: {
id: 3,
title: 'Take dog out',
isComplete: false
}
}
]
}
},
mounted() {
const disableArr = this.todos.map((el) => true)
this.btnDisabled.push(...disableArr)
},
methods: {
addTask(title) {
this.todos.push({
task: {
id: Math.ceil(Math.random() * 1000000),
title,
isComplete: false
}
})
},
deleteTask(id) {
const index = this.todos.findIndex((item) => item.id === id)
this.todos.splice(index, 1)
},
checkBoxChecked(event, index) {
if (event.target.checked) {
this.btnDisabled = this.btnDisabled.map((el, i) => index !== i)
} else {
this.btnDisabled = this.btnDisabled.map((el, i) => index === i)
}
}
}
}
</script>
I think it is because you only just have one boolean state for all of your todos. Maybe it will work if you change btnDisabled data into array, so each todo will have their own disabled or not disabled state on their own.
Here are one way of what I'm trying to point out
Change btnDisabled into array
btnDisabled = this.todos.map(el => true)
Add index to v-for
v-for="(todo,index) in todos"
Pass index to #change
#change="checkBoxChecked($event,index)"
Modify checkBoxChecked Method
checkBoxChecked(event, index) {
if (event.target.checked) {
this.btnDisabled = this.btnDisabled.map((el,i) => index === i ? false : true)
} else {
this.btnDisabled = this.btnDisabled.map((el,i) => index === i ? true : false)
}
}
bind disabled to specific index of btnDisabled
:disabled="btnDisabled[index]"
Hope this solve your issue

how can i change dom element with component

import {
NotificationIcon,
SummaryIcon,
PublishIcon,
EngageIcon,
ListenIcon,
ReportIcon,
PlusIcon,
MinusIcon,
} from "../../icons/Icons";
import "./Sidebar.scss";
import ReactDOM from "react-dom";
const Sidebar = () => {
// handle accordion menu's open effect
const handleClassName = (arg) => {
const element = document.getElementById(arg);
element.classList.toggle("show");
element.firstElementChild.lastElementChild.remove()
};
const handleBrandsIcon = (arg) => {
const allBrands = document.querySelectorAll(".sidebar__brand");
for (let i = 0; i < allBrands.length; i++) {
allBrands[i].classList.remove("active");
document.getElementById(arg).classList.add("active");
}
};
const contentBox = [
{
id: 0,
icon: <SummaryIcon className="sidebar__icon" />,
label: "SUMMARY",
iconType: "summary",
},
{
id: 1,
icon: <PublishIcon className="sidebar__icon" />,
label: "PUBLISH",
iconType: "publish",
},
{
id: 2,
icon: <EngageIcon className="sidebar__icon" />,
label: "ENGAGE",
iconType: "engage",
},
{
id: 3,
icon: <ListenIcon className="sidebar__icon" />,
label: "LISTEN",
iconType: "listen",
},
{
id: 4,
icon: <ReportIcon className="sidebar__icon" />,
label: "REPORT",
iconType: "report",
},
];
const brands = [
{
img: "./gucci.gif",
id: 10,
},
{
img: "./coca-cola.gif",
id: 11,
},
{
img: "./pepsi.gif",
id: 12,
},
{
img: "./samsung.png",
id: 13,
},
{
img: "./tesla.gif",
id: 14,
},
{
img: "./twitter.png",
id: 15,
},
];
return (
<div className="sidebar">
<h2 className="sidebar__header">
sociality<label>.io</label>
</h2>
<div className="sidebar__wrapper">
<div className="sidebar__brands">
{brands.map((brand) => {
return (
<div
id={brand.id}
className="sidebar__brand"
onClick={() => handleBrandsIcon(brand.id)}
>
<img src={brand.img} className="sidebar__img" alt="/" />
</div>
);
})}
</div>
<div className="sidebar__accordion">
<div className="sidebar__content-box">
<div className="sidebar__row">
<NotificationIcon />
<label className="sidebar__label">NOTIFICATIONS</label>
<label className="sidebar__label sidebar__label--rounded">
28
</label>
</div>
<ul className="sidebar__list">
<li className="sidebar__item">Compase</li>
<li className="sidebar__item">Feed</li>
</ul>
</div>
{contentBox.map((content) => {
return (
<div
id={content.id}
className="sidebar__content-box"
onClick={() => handleClassName(content.id)}
>
<div className="sidebar__row">
{content.icon}
<label className="sidebar__label">{content.label}</label>
<PlusIcon className="sidebar__plus" />
</div>
<ul className="sidebar__list">
<li className="sidebar__item">Compase</li>
<li className="sidebar__item">Feed</li>
</ul>
</div>
);
})}
</div>
</div>
</div>
);
};
export default Sidebar;
Hi guys have a good day.When i click to div element which has .sidebar__row class name i want to change <PlusIcon/> with <MinusIcon/>.I progressed it to the phase of remove <PlusIcon/> component but i couldnt any way to add <MinusIcon/> component instead of <PlusIcon/>.In addition i tried add ReactDOM.render(<MinusIcon className="sidebar__plus" /> , element.firstChild) end of handleClassName function and i could add instead of but this time all children elements of <div className="sidebar__row"> have been deleted.Finally if u see any absurd things in my code can u give me advice to write more clean code.
Add state to hold the current selected brand id and conditionally add the "active" class if the currently mapped brand id matches the state.
Add state to hold a map of toggled content ids, and conditionally render the unordered list and plus/minus icon on the current content's id match.
The contentBox and brands arrays are static so they can be pulled out of the component, defined outside it.
const contentBox = [.....];
const brands = [.....];
const Sidebar = () => {
const [showBrandId, setShowBrandId] = React.useState(null);
const [showContentIds, setShowContentIds] = React.useState({});
// handle accordion menu's open effect
const handleClassName = (contentId) => {
setShowContentIds(ids => ({
...ids,
[contentId]: !ids[contentId], // toggles boolean
}));
};
const handleBrandsIcon = (brandId) => {
setShowBrandId(brandId); // replaces current active brand
};
return (
<div className="sidebar">
<h2 className="sidebar__header">
sociality<label>.io</label>
</h2>
<div className="sidebar__wrapper">
<div className="sidebar__brands">
{brands.map((brand) => {
return (
<div
id={brand.id}
className={"sidebar__brand " + brand.id === showBrandId ? "active" : ""}
onClick={() => handleBrandsIcon(brand.id)}
>
<img src={brand.img} className="sidebar__img" alt="/" />
</div>
);
})}
</div>
<div className="sidebar__accordion">
<div className="sidebar__content-box">
<div className="sidebar__row">
<NotificationIcon />
<label className="sidebar__label">NOTIFICATIONS</label>
<label className="sidebar__label sidebar__label--rounded">
28
</label>
</div>
<ul className="sidebar__list">
<li className="sidebar__item">Compase</li>
<li className="sidebar__item">Feed</li>
</ul>
</div>
{contentBox.map((content) => {
const showContent = showContentIds[content.id];
return (
<div
id={content.id}
className={"sidebar__content-box" + showContent ? "show" : ""}
onClick={() => handleClassName(content.id)}
>
<div className="sidebar__row">
{content.icon}
<label className="sidebar__label">{content.label}</label>
{showContent ? (
<MinusIcon className="sidebar__minus" />
) : (
<PlusIcon className="sidebar__plus" />
)}
</div>
{showContent && (
<ul className="sidebar__list">
<li className="sidebar__item">Compase</li>
<li className="sidebar__item">Feed</li>
</ul>
)}
</div>
);
})}
</div>
</div>
</div>
);
};

How to clear Multiselect Dropdown in Semantic UI React using a button?

I have a Semantic UI React Multiselect Dropdown inside a React functional component and want to have buttons inside the menu, as such (still need to center them...)
How can I clear the selected values using the 'Clear' button?
I am able to clear the selection using the 'x' icon, but that's built into the component.
<Dropdown
search
multiple
selection
clearable
closeOnSelectionChange={false}
options={filterInitialSuggestions()}
className='selectDropdown'
header={dropdownButtons()}
/>
const dropdownButtons = () => {
return (
<div>
<Button positive size='mini'>
Save
</Button>
<Button grey size='mini' onClick={() => console.log('I want to reset the multi select dropdown')}>
Clear
</Button>
<Divider />
</div>
);
};
'Save' and 'Clear' buttons with React useState(). In Reactjs you don't need to use DOM queryselector.
https://codesandbox.io/s/white-leftpad-q6re3?file=/src/Fun.jsx
import React, { Component } from 'react';
import { Dropdown } from 'semantic-ui-react';
const options = [
{ key: 1, text: 'Choice 1', value: 1 },
{ key: 2, text: 'Choice 2', value: 2 },
{ key: 3, text: 'Choice 3', value: 3 },
{ key: 4, text: 'Choice 4', value: 4 },
{ key: 5, text: 'Choice 5', value: 5 },
{ key: 6, text: 'Choice 6', value: 6 },
]
class Example extends Component {
state = {
dropval: []
}
onDropChange = (e, { value }) => {
this.setState(
(prevState) => { return { ...prevState, dropval: value } },
// () => console.log(this.state)
)
}
render() {
return (
<div>
<Dropdown
search
multiple
selection
clearable
closeOnSelectionChange={false}
options={options}
className='selectDropdown'
onChange={this.onDropChange}
value={this.state.dropval}
style={{ width: 300 }}
/>
</div>
);
}
}
export default Example;
I figured out how to solve this issue. I'm not sure if this is the best way, but it seems to work decently.
const dropdownButtons = () => {
return (
<>
<div className='dropdown-saveButton'>
<Button
positive
size='mini'
onClick={() => {
saveValues();
}}
>
Save
</Button>
<Button size='mini' onClick={clearDropdown}>
Clear
</Button>
</div>
<Divider inverted />
</>
);
};
const clearDropdown = e => {
var element = dropdownRef.current.querySelector('[aria-selected="true"]');
if (element) {
dropdownRef.current.querySelector('.clear')?.click();
}
};
<Dropdown
multiple
selection
fluid
clearable
header={dropdownButtons} />

Categories

Resources