React changing values without actually changing it - javascript

I am trying to make a game like purple pairs of the purple palace game. What I am trying to do is that whenever two elements are clicked which are not equal in value, then the cards should automatically close but what is happening is something different. Whenever I clicked two wrong cards, then chose a different card then the value is getting changed even though I have not written any code to do so. This is very frustrating, I am getting nowhere trying to solve this. Please help me solve this.
Original Code Pen link Click here to visit
I think the problem lies somewhere in handleClick function.
function Card(props) {
const [show, setShow] = useState(props.chosen);
handleClick = (e) => {
if (props.chosen) {
setShow(true);
} else {
props.onClick();
setShow(!show);
}
};
const style1 = {
background: "grey",
transform: `rotateY(${!show ? 0 : 180}deg)`
};
const style2 = {
background: "#aaa",
transform: `rotateY(${show ? 0 : 180}deg)`
};
return (
<div class="container" onClick={handleClick}>
<div className="flip" style={style1}></div>
<div className="flip" style={style2}>
{props.value}
</div>
</div>
);
}
class GameBoard extends React.Component {
constructor() {
super();
this.state = {
score: 0,
time: 0,
list: [...generateObList(), ...generateObList()],
count: 0
};
}
handleClick = async (id) => {
await this.clickBtn(id);
const list = _.cloneDeep(this.state.list);
const current = list.find((a) => a.id === id);
for (let x of list) {
if (
x.clicked &&
x.id != id &&
x.value == list.find((a) => a.id == id).value
) {
x.chosen = true;
x.clicked = false;
current.chosen = true;
current.clicked = false;
this.setState((prev) => ({
list: prev.list.map((el) =>
el.id === id ? current : el.value === current.value ? x : el
),
score: prev.score + 1
}));
} else if (this.state.count % 2 == 0 && x.clicked) {
console.log("Entered");
current.clicked = false;
x.clicked = false;
this.setState((prev) => ({
list: prev.list.map((el) =>
el.id === id ? current : el.value === current.value ? x : el
)
}));
}
}
};
clickBtn = (id) => {
const current = _.cloneDeep(this.state.list).find((e) => e.id === id);
let deClick = current.clicked;
current.clicked = !current.clicked;
this.setState((prev) => ({
list: prev.list.map((el) => (el.id === id ? current : el)),
count: prev.count + (deClick ? -1 : 1)
}));
};
render() {
const boardStyle = {
gridTemplateColumns: `repeat(5, 1fr)`,
gridTemplateRows: `repeat(5,1r)`
};
let list = this.state.list.map((n) => (
<Card
value={n.value}
onClick={(e) => {
this.handleClick(n.id);
}}
chosen={n.chosen}
clicked={n.clicked}
/>
));
return (
<div class="gameBoard" style={boardStyle}>
{list}
</div>
);
}
}

There were some serious issues in the handleClick function. The main thing that went wrong was that you were somehow managing to replace list items with other list items.
The overwrite issue is happening in this line. I'm not entirely sure why this is causing the issue, but it is.
list: prev.list.map((el) =>
el.id === id ? current : el.value === current.value ? x : el
)
If you just replace it with the following, then the issue dissapears:
list: prev.list.map((el) => el.clicked ? {...el, clicked:false}: el)
The clickBtn wasn't an async function, so using await on it wouldn't do anything. If you want to await for the state to change, you need to resolve a promise. I haven't worked with class components in a while, so I don't know if this would be a particularly encouraged way of working with them, but there are likely other ways:
await new Promise((resolve) =>
this.setState(
(prev) => ({
list: prev.list.map((card) =>
card.clicked ? { ...card, clicked: false } : card
),
freeze: false
}),
() => resolve()
)
);
Another thing to note is that you were keeping the state of which cards were clicked in the GameBoard, so there was no reason to have Card be stateful, in fact, that's the reason why cards wouldn't flip back over.
By changing the start of cards from using useState to just the props values, that's fixed:
const show = props.chosen||props.clicked;
handleClick = (e) => {
if (props.chosen) {
} else {
props.onClick();
}
};
https://codepen.io/ZachHaber/pen/yLJGayv
Refactors:
I did some refactoring to get everything working while I was figuring out what went wrong.
I also had some fun implementing logic, which is why I added the flipping behavior with a timeout when the user guesses wrong.
const {shuffle} = _;
const numbersList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let id = 0;
const generateObList = () => {
return numbersList.map((e) => ({
// Use something more guaranteed to not be the same value
// Math.random *could* clash, but is very unlikely to.
id: id++,
value: e,
chosen: false,
clicked: false
}));
};
// Using a shuffle algorithm here to shuffle the tiles.
const generateList = () => shuffle([...generateObList(), ...generateObList()]);
function Card(props) {
const show = props.chosen || props.clicked;
// Remove the local state here, it's just problematic
// Let the parent control the state.
const handleClick = (e) => {
if (props.chosen) {
} else {
props.onClick();
}
};
const style1 = {
background: "grey",
transform: `rotateY(${!show ? 0 : 180}deg)`
};
const style2 = {
background: "#aaa",
transform: `rotateY(${show ? 0 : 180}deg)`
};
return (
<div className="container" onClick={handleClick}>
<div className="flip" style={style1}></div>
<div className="flip" style={style2}>
{props.value}
</div>
</div>
);
}
class GameBoard extends React.Component {
constructor() {
super();
this.state = {
score: 0,
time: 0,
list: generateList(),
count: 0,
freeze: false
};
}
timerId = null;
performUpdate = (id) => {
// Flip the relevant card in the list
const list = this.state.list.map((card) =>
card.id === id ? { ...card, clicked: !card.clicked } : card
);
// Get the active card
const current = list.find((card) => card.id === id);
// Get all cards that match the current value
let matches = list.filter((card) => card.value === current.value);
// Somehow the card was already chosen
// Likely can remove this because this condition is also in the children
if (matches.every((card) => card.chosen)) {
return; // invalid click, don't do anything
}
// the matches are all clicked, now they are valid to be chosen!
if (matches.every((card) => card.clicked)) {
this.setState((prev) => ({
list: list.map((card) =>
card.value !== current.value
? card
: { ...card, clicked: false, chosen: true }
),
score: prev.score + 1,
count: prev.count + 1
}));
return;
}
// There are 2 cards clicked, reset them after a timer!
if (list.filter((card) => card.clicked).length === 2) {
// Have to post the current click state change -
// Make it so it will flip over the tile briefly
this.setState((prev) => ({ list, count: prev.count + 1, freeze: true }));
// Then after a timeout, flip it back over
this.timerId = setTimeout(() => {
this.setState((prev) => ({
list: prev.list.map((card) =>
card.clicked ? { ...card, clicked: false } : card
),
freeze: false
}));
}, 500);
return;
}
// At this point it's just a normal click:
// set the new adjusted list, and increment count.
this.setState((prev) => ({
list,
count: prev.count + 1
}));
};
handleClick = (id) => {
// Waiting for board to flip tiles over currently. User is impatient
// Could just return in this case if you want users to wait
if (this.state.freeze) {
clearTimeout(this.timerId);
// Perform the update to clear the clicked status and freeze status
// and wait for it to resolve before continuing
this.setState(
(prev) => ({
list: prev.list.map((card) =>
card.clicked ? { ...card, clicked: false } : card
),
freeze: false
}),
() => this.performUpdate(id)
);
} else {
this.performUpdate(id);
}
};
reset = () => {
this.setState({
count: 0,
score: 0,
time: 0,
freeze: false,
list: generateList()
});
};
render() {
const boardStyle = {
gridTemplateColumns: `repeat(5, 1fr)`,
gridTemplateRows: `repeat(5,1r)`
};
let list = this.state.list.map((card) => (
<Card
key={card.id}
value={card.value}
onClick={(e) => {
this.handleClick(card.id);
}}
chosen={card.chosen}
clicked={card.clicked}
/>
));
return (
<div>
<div className="gameBoard" style={boardStyle}>
{list}
</div>
<div>
move count: {this.state.count}
<br />
score: {this.state.score}
</div>
{this.state.score === this.state.list.length / 2 && (
<button onClick={this.reset}>Reset</button>
)}
</div>
);
}
}
ReactDOM.render(<GameBoard/>,document.getElementById('root'))
.container {
position: relative;
width: 100px;
height: 100px;
background: #eee;
cursor: pointer;
}
.gameBoard {
display: grid;
}
.container .flip {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 100px;
height: 100px;
font-size: 4em;
transition: transform 400ms;
transition-timing-function: cubic-bezier(0.1, 0.39, 0.3, 0.95);
backface-visibility: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"/>

Related

How to use useDraggable / useSortable hooks of dnd-kit library properly?

I'm trying to create a simple calculator with React and dnd-kit. Elements of calculator can be dragged to droppable area and can be sorted inside of it. You can see a problem on the gif: when I drag element from left side to droppable area, there is no animation of dragging but element can be dropped to area. And inside of droppable area elements can be beautifly sorted with animation of dragging.
So, I need drag animation to work when I drag elements from left side to droppable area.
Code for App component:
const App: FC = () => {
const [selected, setSelected] = useState('Constructor')
const [droppedElems, setDroppedElems] = useState<CalcElemListInterface[]>([])
const handleActiveSwitcher = (id: string) => {
setSelected(id)
}
const deleteDroppedElem = (item: CalcElemListInterface) => {
const filtered = [...droppedElems].filter(elem => elem.id !== item.id)
setDroppedElems(filtered)
}
const leftFieldStyles = cn(styles.left, {
[styles.hidden]: selected === 'Runtime'
})
const calcElementsList = calcElemListArray.map((item) => {
const index = droppedElems.findIndex(elem => elem.id === item.id)
const layoutDisabledStyle = index !== -1
return (
<CalcElemLayout
key={item.id}
id={item.id}
item={item}
layoutDisabledStyle={layoutDisabledStyle}
/>
)
})
const handleDragEnd = (event: DragEndEvent) => {
const { id, list } = event.active.data.current as CalcElemListInterface
const elem = {id, list}
if (event.over && event.over.id === 'droppable') {
setDroppedElems((prev) => {
return [...prev, elem]
})
}
}
return (
<div className={styles.layout}>
<div className={styles.top}>
<Switcher
selected={selected}
handleActiveSwitcher={handleActiveSwitcher}
/>
</div>
<DndContext
onDragEnd={handleDragEnd}
>
<div className={styles.content}>
<div className={leftFieldStyles}>
{calcElementsList}
</div>
<DropElemLayout
deleteDroppedElem={deleteDroppedElem}
selected={selected}
droppedElems={droppedElems}
setDroppedElems={setDroppedElems}
/>
</div>
</DndContext>
</div>
)
}
Code for droppable area:
const DropElemLayout: FC<DropElemLayoutInterface> = ({ selected, droppedElems, deleteDroppedElem, setDroppedElems }) => {
const { isOver, setNodeRef } = useDroppable({
id: 'droppable'
})
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
)
const style = {
backgroundColor: (isOver && !droppedElems.length) ? '#F0F9FF' : undefined,
}
const droppedRuntimeElemList = droppedElems.map((item) => {
const layoutEnabledStyle = droppedElems.length ? true : false
return (
<CalcElemLayout
key={item.id}
id={item.id}
item={item}
deleteDroppedElem={deleteDroppedElem}
selected={selected}
layoutEnabledStyle={layoutEnabledStyle}
/>
)
})
const droppedElemList = !droppedElems.length
?
<div className={styles.rightContent}>
<Icon name="#drop"/>
<p>Перетащите сюда</p>
<span>любой элемент</span>
<span>из левой панели</span>
</div>
:
droppedRuntimeElemList
const className = !droppedElems.length ? styles.right : styles.left
const handleDragEnd = (event: DragEndEvent) => {
if (event.active.id !== event.over?.id) {
setDroppedElems((items: CalcElemListInterface[]) => {
const oldIndex = items.findIndex(item => item.id === event.active?.id)
const newIndex = items.findIndex(item => item.id === event.over?.id)
return arrayMove(items, oldIndex, newIndex)
})
}
}
return (
<DndContext
onDragEnd={handleDragEnd}
sensors={sensors}
collisionDetection={closestCenter}
>
<div
ref={setNodeRef}
className={className}
style={style}
>
<SortableContext
items={droppedElems}
strategy={verticalListSortingStrategy}
>
{droppedElemList}
</SortableContext>
</div>
</DndContext>
)
}
Code for Element itself:
const CalcElemLayout: FC<CalcElemLayoutInterface> = ({ item, id, deleteDroppedElem, selected, layoutDisabledStyle, layoutEnabledStyle }) => {
const { current } = useAppSelector(state => state.calculator)
// const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
// id: id,
// data: {...item},
// disabled: selected === 'Runtime'
// })
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
} = useSortable({
id: id,
data: {...item},
disabled: selected === 'Runtime'
})
const style = {
transform: CSS.Translate.toString(transform),
transition: transition
}
const handleDeleteDroppedElem = () => {
deleteDroppedElem?.(item)
}
const doubleClickCondition = selected === 'Constructor' ? handleDeleteDroppedElem : undefined
const layoutStyle = cn(styles.elemLayout, {
[styles.operators]: item.id === 'operators',
[styles.digits]: item.id === 'digits',
[styles.equal]: item.id === 'equal',
[styles.disabled]: layoutDisabledStyle,
[styles.enabled]: layoutEnabledStyle,
})
const buttonList = item.list?.map(elem => (
<Button
key={elem.name}
elem={elem.name}
selected={selected!}
/>
))
const resultStyle = cn(styles.result, {
[styles.minified]: current.length >= 10
})
const elemList = item.id === 'result'
?
<div className={resultStyle}>{current}</div>
:
buttonList
const overlayStyle = {
opacity: '0.5',
}
return (
<>
<div
ref={setNodeRef}
className={layoutStyle}
onDoubleClick={doubleClickCondition}
style={style}
{...attributes}
{...listeners}
>
{elemList}
</div>
</>
)
}
All you need to do is to add DragOverlay component properly in Element like so:
const CalcElemLayout: FC<CalcElemLayoutInterface> = ({ item, id, deleteDroppedElem, selected, layoutDisabledStyle, layoutEnabledStyle }) => {
const { current } = useAppSelector(state => state.calculator)
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
} = useSortable({
id: id,
data: {...item},
disabled: selected === 'Runtime',
})
const handleDeleteDroppedElem = () => {
deleteDroppedElem?.(item)
}
const doubleClickCondition = selected === 'Constructor' ? handleDeleteDroppedElem : undefined
const layoutStyle = cn(styles.elemLayout, {
[styles.operators]: item.id === 'operators',
[styles.digits]: item.id === 'digits',
[styles.equal]: item.id === 'equal',
[styles.disabled]: layoutDisabledStyle,
[styles.enabled]: layoutEnabledStyle,
[styles.transparent]: isDragging
})
const style = {
transform: CSS.Translate.toString(transform),
transition: transition
}
const buttonList = item.list?.map(elem => (
<Button
key={elem.name}
elem={elem.name}
selected={selected!}
/>
))
const resultStyle = cn(styles.result, {
[styles.minified]: current.length >= 10
})
const elemList = item.id === 'result'
?
<div className={resultStyle}>{current}</div>
:
buttonList
const dragOverlayContent = isDragging
?
<div
className={layoutStyle}
style={{
opacity: isDragging ? '1' : '',
boxShadow: isDragging ? '0px 2px 4px rgba(0, 0, 0, 0.06), 0px 4px 6px rgba(0, 0, 0, 0.1)' : ''
}}
>
{elemList}
</div>
:
null
return (
<>
<div
ref={setNodeRef}
className={layoutStyle}
onDoubleClick={doubleClickCondition}
style={style}
{...attributes}
{...listeners}
>
{elemList}
</div>
<DragOverlay dropAnimation={null}>
{dragOverlayContent}
</DragOverlay>
</>
)
}

React setting state to child component causing infinite loop

I have a page with some dropdown menu's used to search some content, the dropdown is a non-functional component. The page is a listsing page. Not important but gives some context.
I do some calculation on the listing page and update the state, then I pass this state into the Dropdown component. However, I'm getting an infinite loop and I'm not sure how to stop it or where I'm going wrong.
my listing page is here:
constructor(props){
super(props)
let industryList = this.createList(this.props.data.mainYaml.caseStudiesDropdowns[0].items)
let areaList = this.createList(this.props.data.mainYaml.caseStudiesDropdowns[1].items)
let techniqueList = this.createList(this.props.data.mainYaml.caseStudiesDropdowns[2].items)
this.state = {
industry: "All Industries",
area: "All Areas",
technique: [],
industries: industryList,
areas: areaList,
techniques: techniqueList
}
}
createList = (listItems) => {
let listArr = []
listItems.forEach((item) => {
let obj = {
name: item,
disabled: false
}
listArr.push(obj)
})
return listArr
}
filterCaseStudies = (caseStudies) => {
const filterIndustry = (caseStudies) => {
if (this.state.industry == "All Industries") {
return caseStudies
} else {
return caseStudies.filter((study) => study.node.industry == this.state.industry)
}
}
const filterArea = (caseStudies) => {
if (this.state.area == "All Areas") {
return caseStudies
} else {
return caseStudies.filter((study) => study.node.area == this.state.area)
}
}
const filterTechnique = (caseStudies) => {
if (this.state.technique.length === 0) {
return caseStudies
} else {
let matchedStudies = []
caseStudies.forEach((study) => {
let count = 0;
let techCount = study.node.technique.length - 1;
study.node.technique.forEach((item, i) => {
this.state.technique.forEach((selectedItems) => {
if (selectedItems == item) {
count++;
return
}
})
if (i == techCount && count > 0) {
study.node.count = count
matchedStudies.push(study)
}
})
})
matchedStudies.sort((a, b) => b.node.count - a.node.count);
return matchedStudies;
}
}
let industryMatches = filterIndustry(caseStudies)
let areaMatches = filterArea(industryMatches)
this.filterDropdowns(areaMatches)
let techniqueMatches = filterTechnique(areaMatches)
return techniqueMatches;
}
filterDropdowns = (filteredCaseStudies) => {
console.log(filteredCaseStudies)
let disabledIndustries = [];
let disabledAreas = [];
let disabledTechniques = [];
this.state.industries.forEach((industry) => {
let obj = {
name: industry.name
}
if (industry.name == "All Industries") {
console.log(industry.name)
obj.disabled = false;
disabledIndustries.push(obj);
} else {
obj.disabled = true;
filteredCaseStudies.forEach((study) => {
if (study.node.industry == industry.name) {
obj.disabled = false;
}
})
disabledIndustries.push(obj);
}
})
console.log(disabledIndustries)
this.setState({industries: disabledIndustries})
}
getCaseStudies = (caseStudies) => {
let filteredCaseStudies = this.filterCaseStudies(caseStudies)
if (filteredCaseStudies.length > 0) {
return filteredCaseStudies.map((study, i) => {
return (
<div key={i} className="col-lg-4 col-md-6 col-12 px-4 mb-5">
<CaseStudyListItem
data={study.node}
className="CaseStudyListItem--lg"
index={i}/>
</div>
)
})
} else {
return (
<div className="col-12 px-4 mb-5">
<h4>We're Sorry!</h4>
<p>We can't seem to find any case studies that match your search. Please try other search terms.</p>
</div>
)
}
}
dropdownChange = (selected, name) => {
this.setState({[name]: selected})
}
render () {
console.log(this.state)
return (
<Layout bodyClass="k-reverse-header">
<div className="CaseStudies">
<section className="CaseStudies__header k-bg--grey">
<div className="container-fluid">
<div className="d-flex k-row">
<div className="col-12 px-4">
<DropdownSelect className="CaseStudies__search-industry mb-4" data={this.props.data.mainYaml.caseStudiesDropdowns[0]} list={this.state.industries} selected={this.dropdownChange} />
<DropdownSelect className="CaseStudies__search-area mb-4" data={this.props.data.mainYaml.caseStudiesDropdowns[1]} list={this.state.areas} selected={this.dropdownChange} />
</div>
</div>
</div>
</section>
<section className="CaseStudies__list">
<div className="container-fluid">
<div className="d-flex flex-wrap k-row">
{this.getCaseStudies(this.props.data.allCaseStudiesYaml.edges)}
</div>
</div>
</section>
</div>
</Layout>
)
}
}
I believe the issue happens as I pass the state into the Dropdown component, it is also updated in the filterDropdowns function. The Dropdown component code is as follows.
const DropdownSelect = ({ data, className, list, selected}) => {
const [isActive, setActive] = useState(false);
const [activeItem, changeActiveItem] = useState(data.placeholder);
const ref = useRef();
useEffect(() => {
const checkIfClickedOutside = (e) => {
// If the menu is open and the clicked target is not within the menu,
// then close the menu
if (isActive && ref.current && !ref.current.contains(e.target)) {
setActive(false)
}
}
document.addEventListener("mousedown", checkIfClickedOutside)
return () => {
// Cleanup the event listener
document.removeEventListener("mousedown", checkIfClickedOutside)
}
}, [isActive])
const toggleClass = () => {
setActive(!isActive);
}
const buildDropdown = () => {
const splitArr = (arr, len) => {
let chunks = [], i = 0, n = arr.length;
while (i < n) {
chunks.push(arr.slice(i, i += len));
}
return chunks;
}
const buildList = (items) => {
return items.map((item, i) =>
<li
key={i}
className={`DropdownSelect__list-item ${activeItem == item.name ? "active" : ""} ${item.disabled ? "disabled" : ""}`}
onClick={() => itemClicked(item.name, selected, data.name)}
>
{item.name}
</li>
)
}
const itemClicked = (item, selected, search) => {
changeActiveItem(item)
selected(item, search)
}
const arrLen = list.length < 10 ? 3 : 4;
const listsArr = splitArr(list, arrLen);
return listsArr.map((list, i) =>
<ul key={i} className="DropdownSelect__list">
{buildList(list)}
</ul>
)
}
return (
<div className={`DropdownSelect ${className ? className : ''}`} ref={ref}>
<div
className={`DropdownSelect__button ${isActive ? "active" : ""}`}
onClick={toggleClass}
>
{activeItem == null ? data.placeholder : activeItem}
</div>
<div className={`DropdownSelect__list-wrapper ${isActive ? "active" : ""}`}>{buildDropdown}</div>
</div>
)
}
export default DropdownSelect
I feel like i could have all of my state in the listing page but then the Dropdown component is pointless as it wouldn't be self sufficient and usable elsewhere.
I guess I want to know how I break this loop but also what are my bad practices here? ie am I using state wrongly?
Any help greatly appreciated!
PS Here's the React error i get
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
If I'm reading this right...
In your render, you have {this.getCaseStudies(this.props.data.allCaseStudiesYaml.edges)}. getCaseStudies calls filterCaseStudies, which calls filterDropdowns, which has a setState in it. When a setState occurs, the page re-renders, causing the page to go through all those function calls again, another setState occurs, the page re-renders again, forever, causing an infinite loop.
You'll have to re-write your code somewhat. You could possibly use that state to store the data in a different format, like an array, and map the data in your render?

React accordion get collapsible height

I'm trying to get height of the collapsible but it always same value which is uncollapsed div's height.
Is there a way to get actual content height of div inside the accordion ?
When I print current ref's height it always returns a fixed value. But if I click the same collapsible it returns correct value so I guess it takes uncollapsed div height of collapsible.
Accordion.js
const Accordion = ({ children }) => {
const [ selected, select ] = useState(null);
const [ currentState, changeCurrentState ] = useState(true);
const onCollapsibleSelected = (selectedItem, selectedState, ref) => {
select(selectedItem);
changeCurrentState(!selectedState);
// Always same - because accordion is not expanded yet
console.log(ref.current.offsetHeight)
};
const collapsibleChildren = children.map((item, index) => {
let collapsible;
if (item && item.props) {
const hidden = selected && selected === index ? currentState : true;
collapsible = (
<Collapsible { ...item.props }
onSelect={ onCollapsibleSelected }
key={ index }
index={ index }
collapsed={ hidden }
/>
);
}
return collapsible;
});
return (
<Fragment>
{ collapsibleChildren }
</Fragment>
);
};
Accordion.displayName = 'Accordion';
Accordion.propTypes = {
children: PropTypes.any
};
export default Accordion;
.. index.js
const Collapsible = ({ content, title, onSelect, collapsed, index }) => {
const collapsibleRef = useRef();
const trackCollapsibleBlockClick = () => {
const event = collapsed ? OPEN_COLLAPSIBLE_BLOCK: CLOSE_COLLAPSIBLE_BLOCK;
trackEvent({ ... event, name: title });
};
const onTitleClicked = () => {
trackCollapsibleBlockClick();
onSelect(index, collapsed, collapsibleRef);
};
return (
<CollapsibleStyle ref={ collapsibleRef } onClick={ onTitleClicked }>
<Title collapsed={ collapsed }>{ title }</Title>
<Content hidden={ collapsed }>{ content }</Content>
</CollapsibleStyle>
);
};
Collapsible.displayName = 'Collapsible';
Collapsible.propTypes = {
content: PropTypes.array,
title: PropTypes.string,
onSelect: PropTypes.func,
collapsed: PropTypes.bool,
index: PropTypes.number
};
export default Collapsible;
CollapsibleStyle.js
import styled from 'styled-components';
export default styled.div`
&& {
background-color: ${ ({ theme: { collapsible } }) => collapsible.backgroundColor };
border-radius: ${ ({ theme: { collapsible } }) => collapsible.borderRadius };
margin-bottom: ${ ({ theme: { collapsible } }) => collapsible.marginBottom };
padding: ${ ({ theme: { collapsible } }) => collapsible.paddingVertical }
${ ({ theme: { experience: { wrapper } } }) => wrapper.padding.md };
position: relative;
cursor: pointer;
}
`;
I can actually see the correct height of the child when I print ref.current.children. But when I try to access like ref.current.children[1].clientHeight it returns wrong value.
Solved as below. Using useEffect as it produces lifecycle events. I check if the collapsible item is changed by collapsed check.
useEffect(() => {
handleCollapsibleViewPort();
}, [collapsed]);
const handleCollapsibleViewPort = () => {
if(uncollapsedHeight !== 0 && collapsibleRef.current.clientHeight > uncollapsedHeight ) {
scrollElementToTop(collapsibleRef.current.getBoundingClientRect().top);
}
}

how to use clearInteval let timer clear it self in ReactJS?

I am new to react, I am trying to write a react component, component has several features.
user can input a random number, then number will be displayed in the
page too.
implement a button with text value 'start', once click the button,
the number value displayed will reduce one every 1second and the
text value will become 'stop'.
continue click button, minus one will stop and text value of button
will become back to 'start'.
when number subtracted down to 0 will automatically stop itself.
I have implemented first three features. but I am not sure how do I start the last one. should I set another clearInteval? based on if statement when timer counts down 0?
code is here:
var myTimer;
class App extends Component {
constructor(props) {
super(props);
this.state = {
details: [{ id: 1, number: "" }],
type: false
};
this.handleClick = this.handleClick.bind(this);
}
changeNumber = (e, target) => {
this.setState({
details: this.state.details.map(detail => {
if (detail.id === target.id) {
detail.number = e.target.value;
}
return detail;
})
});
};
handleClick = () => {
this.setState(prevState => ({
type: !prevState.type
}));
if (this.state.type === false) {
myTimer = setInterval(
() =>
this.setState({
details: this.state.details.map(detail => {
if (detail.id) {
detail.number = parseInt(detail.number) - 1;
}
return detail;
})
}),
1000
);
}
if (this.state.type === true) {
clearInterval(myTimer);
}
};
render() {
return (
<div>
{this.state.details.map(detail => {
return (
<div key={detail.id}>
Number:{detail.number}
<input
type="number"
onChange={e => this.changeNumber(e, detail)}
value={detail.number}
/>
<input
type="button"
onClick={() => this.handleClick()}
value={this.state.type ? "stop" : "start"}
/>
</div>
);
})}
</div>
);
}
}
export default App;
just add
if (detail.number === 0) {
clearInterval(myTimer);
}
in
handleClick = () => {
this.setState(prevState => ({
type: !prevState.type
}));
if (this.state.type === false) {
myTimer = setInterval(
() =>
this.setState({
details: this.state.details.map(detail => {
if (detail.id) {
detail.number = parseInt(detail.number) - 1;
if (detail.number === 0) {
clearInterval(myTimer);
}
}
return detail;
})
}),
1000
);
}
if (this.state.type === true) {
clearInterval(myTimer);
}
};
Here You have this solution on Hooks :)
const Test2 = () => {
const [on, setOn] = useState(false)
const initialDetails = [{ id: 1, number: "" }]
const [details, setDetails] = useState(initialDetails)
const changeNumber = (e, target) => {
setDetails({ details: details.map(detail => { if (detail.id === target.id) { detail.number = e.target.value; } return detail; }) });
if (this.state.details.number === 0) { setOn(false) }
};
const handleClick = () => {
if (on === false) {myTimer = setInterval(() =>
setDetails({details: details.map(detail => {if (detail.id) {detail.number = parseInt(detail.number) - 1; if (detail.number === 0) {clearInterval(myTimer);} }
return detail;})}),1000);}
if (on === true) { clearInterval(myTimer); }
};
return (
<div>
{details.map(detail => {
return (
<div key={detail.id}>
Number:{detail.number}
<input
type="number"
onChange={e => changeNumber(e, detail)}
value={detail.number}
/>
<input
type="button"
onClick={() => handleClick()}
value={on ? "stop" : "start"}
/>
</div>
);
})}
</div>
)
}

How to scroll a list item into view using scrollintoview method using reactjs?

i want to move a list item into view using scrollIntoView method using reactjs.
What i am trying to do?
i have an array of objects stored in variable some_arr and i display those values in a dropdown menu. when user presses down key then the dropdown item gets highlighted. also using up arrow key to navigate up in the dropdown menu.
and clicking enter key will select the dropdown item and replaces the value in the input field.
I have implemented code below and it works fine. but when user presses down arrow key and highlighted dropdown menu is not in view i want it to be visible to user.
So to implement that i have used ref (this.dropdown_item_ref) to the dropdown item. however this ref always points to last item in the dropdown menu. meaning consider i have
some_arr = [
{
id:1,
name: somename,
},
{
id: 2,
name: fname,
},
{
id: 3,
name: lname, //ref is always pointing to this item
},
],
so here the ref is always pointing to lname in the dropdown menu.
Below is what i have tried and is not working,
class Dropdownwithinput extends React,PureComponent {
constructor(props) {
super(props);
this.list_item_ref = React.createRef();
this.state = {
input_val: '',
dropdown_values: [],
dropdown_item_selection: 0,
};
}
componentDidMount = () => {
const values = [
{
id:1,
name: somename,
},
{
id: 2,
name: fname,
},
{
id: 3,
name: lname, //ref is always pointing to this item
},
],
this.setState({dropdown_values: values});
}
handle_key_down = (event) => {
if (this.state.dropdown_values > 0) {
if (event.keyCode === 38 && this.state.dropdown_item_selection
> 0) {
this.setState({dropdown_item_selection:
(this.state.dropdown_item_selection - 1) %
this.state.dropdown_values.length});
this.list_item_ref.current.scrollIntoView();
} else if (event.keyCode === 40) {
this.setState({dropdown_item_selection:
(this.state.dropdown_values_selection + 1) %
this.state.dropdown_values.length});
this.list_item_ref.current.scrollIntoView();
}
if (event.keyCode === 13) {
event.preventDefault();
const selected_item =
this.state.dropdown_values[this.state.user_selection];
const text = this.replace(this.state.input_val,
selected_item);
this.setState({
input_val: text,
dropdown_values: [],
});
}
}
replace = (input_val, selected_item) => {
//some function to replace value in input field with the
//selected dropdown item
}
render = () => {
return (
<input
onChange={this.handle_input_change}
onKeyDown={this.handle_key_down}/>
<div>
{this.state.dropdown_values.map((item, index) => (
<div key={index} className={"item" + (index ===
this.state.dropdown_item_selection ? ' highlight'
: '')}>
{item.name}
</div>
))}
</div>
)
};
}
}
Could someone help me fix this. thanks.
I have adapted a bit your code:
import React from "react";
class Example extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
const dropdownValues = Array.from({ length: 100 }, (_, k) => k).reduce(
(acc, curr) => {
return acc.concat([{ id: curr, name: `${curr}.so` }]);
},
[]
);
this.state = {
input_val: "",
dropdownValues,
selectedItem: 0
};
this.listRefs = dropdownValues.reduce((acc, current, index) => {
acc[index] = React.createRef();
return acc;
}, {});
}
componentDidMount() {
window.addEventListener("keydown", this.handleKeyDown);
}
componentWillUnmount() {
window.removeEventListener("keydown", this.handleKeyDown);
}
componentDidUpdate(prevProps, prevState) {
if (prevState.selectedItem !== this.state.selectedItem) {
this.listRefs[this.state.selectedItem].current.scrollIntoView();
}
}
handleKeyDown = event => {
const keyCodes = {
up: 38,
down: 40
};
if (![38, 40].includes(event.keyCode)) {
return;
}
this.setState(prevState => {
const { dropdownValues, selectedItem } = prevState;
let nextSelectedItem;
if (keyCodes.up === event.keyCode) {
nextSelectedItem =
dropdownValues.length - 1 === selectedItem ? 0 : selectedItem + 1;
}
nextSelectedItem =
selectedItem === 0 ? dropdownValues.length - 1 : selectedItem - 1;
return { ...prevState, selectedItem: nextSelectedItem };
});
};
replace = (input_val, selected_item) => {
//some function to replace value in input field with the
//selected dropdown item
};
render() {
return (
<>
<input
onChange={this.handle_input_change}
onKeyDown={this.handle_key_down}
/>
<button
type="button"
onClick={() => this.setState({ selectedItem: 50 })}
>
Focus element 50
</button>
<div ref={this.listRef}>
{this.state.dropdownValues.map((item, index) => (
<div key={index} ref={this.listRefs[index]}>
<div
style={
this.state.selectedItem === index
? { background: "yellow" }
: {}
}
>
{item.name}
</div>
</div>
))}
</div>
</>
);
}
}
export default Example;

Categories