Yo there! Back at it again with a noob question!
So I'm fetching data from an API to render a quizz app and I'm struggling with a simple(I think) function :
I have an array containing 4 answers. This array renders 4 divs (so my answers can each have an individual div). I'd like to change the color of the clicked div so I can verify if the clicked div is the good answer later on.
Problem is when I click, the whole array of answers (the 4 divs) are all changing color.
How can I achieve that?
I've done something like that to the divs I'm rendering :
const [on, setOn] = React.useState(false);
function toggle() {
setOn((prevOn) => !prevOn);
}
const styles = {
backgroundColor: on ? "#D6DBF5" : "transparent",
};
I'll provide the whole code of the component and the API link I'm using at the end of the post so if needed you can see how I render the whole thing.
Maybe it's cause the API lacks an "on" value for its objects? I've tried to assign a boolean value to each of the items but I couldn't seem to make it work.
Thanks in advance for your help!
The whole component :
import React from "react";
import { useRef } from "react";
export default function Quizz(props) {
const [on, setOn] = React.useState(false);
function toggle() {
setOn((prevOn) => !prevOn);
}
const styles = {
backgroundColor: on ? "#D6DBF5" : "transparent",
};
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
let temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
let answers = props.incorrect_answers;
const ref = useRef(false);
if (!ref.current) {
answers.push(props.correct_answer);
shuffleArray(answers);
ref.current = true;
}
const cards = answers.map((answer, key) => (
<div key={key} className="individuals" onClick={toggle} style={styles}>
{answer}
</div>
));
console.log(answers);
console.log(props.correct_answer);
return (
<div className="questions">
<div>
<h2>{props.question}</h2>
</div>
<div className="individuals__container">{cards}</div>
<hr />
</div>
);
}
The API link I'm using : "https://opentdb.com/api.php?amount=5&category=27&type=multiple"
Since your answers are unique in every quizz, you can use them as id, and instead of keeping a boolean value in the state, you can keep the selected answer in the state, and when you want render your JSX you can check the state is the same as current answer or not, if yes then you can change it's background like this:
function Quizz(props) {
const [activeAnswer, setActiveAnswer] = React.useState('');
function toggle(answer) {
setActiveAnswer(answer);
}
...
const cards = answers.map((answer, key) => (
<div key={key}
className="individuals"
onClick={()=> toggle(answer)}
style={{background: answer == activeAnswer ? "#D6DBF5" : "transparent" }}>
{answer}
</div>
));
...
}
Related
I am working on a sidebar using a recursive function to populate a nested list of navigation items.
Functionally, everything works except for the re-render when I click on one of the list items to toggle the visibility of the child list.
Now, when I expand or collapse the sidebar (the parent component with its visibility managed in its own state), the list items then re-render as they should. This shows me the state is being updated.
I have a feeling this possibly has something to do with the recursive function?
import React, { useState } from "react";
import styles from "./SidebarList.module.css";
function SidebarList(props) {
const { data } = props;
const [visible, setVisible] = useState([]);
const toggleVisibility = (e) => {
let value = e.target.innerHTML;
if (visible.includes(value)) {
setVisible((prev) => {
let index = prev.indexOf(value);
let newArray = prev;
newArray.splice(index, 1);
return newArray;
});
} else {
setVisible((prev) => {
let newArray = prev;
newArray.push(value);
return newArray;
});
}
};
const hasChildren = (item) => {
return Array.isArray(item.techniques) && item.techniques.length > 0;
};
const populateList = (data) => {
return data.map((object) => {
return (
<>
<li
key={object.name}
onClick={(e) => toggleVisibility(e)}
>
{object.name}
</li>
{visible.includes(object.name) ? (
<ul id={object.name}>
{hasChildren(object) && populateList(object.techniques)}
</ul>
) : null}
</>
);
});
};
let list = populateList(data);
return <ul>{list}</ul>;
}
export default SidebarList;
There are many anti patterns with this code but I will just focus on rendering issue. Arrays hold order. Your state does not need to be ordered so it's easier to modify it, for the case of demo I will use object. Your toggle method gets event, but you want to get DOM value. That's not necessary, you could just sent your's data unique key.
See this demo as it fixes the issues I mentioned above.
I'm coding a tab navigation system with a sliding animation, the tabs are all visible, but only the selected tab is scrolled to. Problem is that, I need to get the ref of the current selected page, so I can set the overall height of the slide, because that page may be taller or shorter than other tabs.
import React, { MutableRefObject } from 'react';
import Props from './Props';
import styles from './Tabs.module.scss';
export default function Tabs(props: Props) {
const [currTab, setCurrTab] = React.useState(0);
const [tabsWidth, setTabsWidth] = React.useState(0);
const [currentTabHeight, setCurrentTabHeight] = React.useState(0);
const [currentTabElement, setCurrentTabElement] = React.useState<Element | null>(null);
const thisRef = React.useRef<HTMLDivElement>(null);
let currentTabRef = React.useRef<HTMLDivElement>(null);
let refList: MutableRefObject<HTMLDivElement>[] = [];
const calculateSizeData = () => {
if (thisRef.current && tabsWidth !== thisRef.current.offsetWidth) {
setTabsWidth(() => thisRef.current.clientWidth);
}
if (currentTabRef.current && currentTabHeight !== currentTabRef.current.offsetHeight) {
setCurrentTabHeight(() => currentTabRef.current.offsetHeight);
}
}
React.useEffect(() => {
calculateSizeData();
const resizeListener = new ResizeObserver(() => {
calculateSizeData();
});
resizeListener.observe(thisRef.current);
return () => {
resizeListener.disconnect();
}
}, []);
refList.length = 0;
return (
<div ref={thisRef} className={styles._}>
<div className={styles.tabs}>
{ props.tabs.map((tab, index) => {
return (
<button onClick={() => {
setCurrTab(index);
calculateSizeData();
}} className={currTab === index ? styles.tabsButtonActive : ''} key={`nav-${index}`}>
{ tab.label }
<svg>
<rect rx={2} width={'100%'} height={3} />
</svg>
</button>
)
}) }
</div>
<div style={{
height: currentTabHeight + 'px',
}} className={styles.content}>
<div style={{
right: `-${currTab * tabsWidth}px`,
}} className={styles.contentStream}>
{ [ ...props.tabs ].reverse().map((tab, index) => {
const ref = React.useRef<HTMLDivElement>(null);
refList.push(ref);
return (
<div ref={ref} style={{
width: tabsWidth + 'px',
}} key={`body-${index}`}>
{ tab.body }
</div>
);
}) }
</div>
</div>
</div>
);
}
This seems like a reasonable tab implementation for a beginner. It appears you're passing in content for the tabs via a prop named tabs and then keeping track of the active tab via useState() which is fair.
Without looking at the browser console, I believe that React doesn't like the way you are creating the array of refs. Reference semantics are pretty challenging, even for seasoned developers, so you shouldn't beat yourself up over this.
I found a good article that discusses how to keep track of refs to an array of elements, which I suggest you read.
Furthermore, I'll explain the differences between that article and your code. Your issues begin when you write let refList: MutableRefObject<HTMLDivElement>[] = []; According to the React hooks reference, ref objects created by React.useRef() are simply plain JavaScript objects that are persisted for the lifetime of the component. So what happens when we have an array of refs like you do here? Well actually, the contents of the array are irrelevant--it could be an array of strings for all we care. Because refList is not a ref object, it gets regenerated for every render.
What you want to do is write let refList = React.useRef([]), per the article, and then populate refList.current with refs to your child tabs as the article describes. Referring back to the React hooks reference, the object created by useRef() is a plain JavaScript object, and you can assign anything to current--not just DOM elements.
In summary, you want to create a ref of an array of refs, not an array of refs. Repeat that last sentence until it makes sense.
I have a hard time trying to understand "useState" and when a render is triggered. I want to make a drag and drop system where the user can drag elements from one box to another.
I have successfully done this by using pure javascript, but as usual, it gets messy fast and I want to keep it clean. I thought since I'm using react I should do it using UseState, and I've got the array to update the way I want it to but the changes don't render.
Shouldn't I use useState in this way? What should I use instead? I don't want to solve it the "hacky" way, I want it to be proper.
const [allComponents, updateArray] = useState([])
function arrayMove(array, closest) {
let moveChild = array[parentIndex].children[childIndex]
array[parentIndex].children = array[parentIndex].children.filter(function(value, index, arr){ return index != childIndex;});
array[closest].children = [...array[closest].children, moveChild];
return array;
}
var lastClosest = {"parent": null, "child": null};
var parentIndex = null;
var childIndex = null;
function allowDrop(ev, index) {
ev.preventDefault();
if(allComponents.length > 0) {
let closest = index;
if((parentIndex == lastClosest.parent && childIndex == lastClosest.child) || (closest == parentIndex)) {
return;
}
lastClosest.parent = parentIndex;
lastClosest.child = childIndex;
updateArray(prevItems => (arrayMove(prevItems, closest)));
}
}
function dragStart(pI, cI, ev) {
parentIndex = pI;
childIndex = cI;
}
function drop(ev) {
ev.preventDefault();
}
function addNewSpace() {
updateArray(prevItems => [...prevItems, {"name": "blaab", "children": [{"child": "sdaasd", "type": "text"}]}]);
}
return (
<div className={styles.container}>
<button onClick={addNewSpace}>Add</button>
<div className={styles.spacesWrapper}>
{allComponents.map(({name, children}, index) => (
<div key={"spacer_" + index} onDragOver={(event) => allowDrop(event, index)} onDrop={(event) => drop(event)} className={styles.space}>
{children.map(({type, child}, index2) => (
<div id ={"movable_" + index + ":" + index2} key={"movable_" + index2} className={styles.moveableOne}>
<div key={"draggable_" + index2} draggable="true" onDrag={(event) => onDrag(event)} onDragStart={(event) => dragStart(index, index2, event)}>
</div>
</div>
))}
</div>
))}
</div>
</div>
)
}
Here is the problem I'm experiencing with the nested array not updating properly:
A react page re-renders every time the state changes. So every time a setState is called, the react component re-renders itself and updates the page to reflect the changes.
You can also see how the state is updating if you install the react dev tools chrome extension.
Try to modify your code with the below code and see if it solves your issue,
import React, { useState, useEffect } from "react";
let newAllComponents = [...allComponents];
useEffect(() =>
{
newAllComponents = [...allComponents]
}, [allComponents]);
/* div inside return*/
{newAllComponents.map(({ name, children }, index) => (
<div
key={"spacer_" + index}
onDragOver={(event) => allowDrop(event, index)}
onDrop={(event) => drop(event)}
>
I am still very new to this and probably completely overthinking/overcomplicating things.
I have an array of images which display as expected. As part of the mapping process, I create a new ref for each image, so that I can tap into the 'current' attribute to retrieve height and width, based on which I have a ternary operator to apply some styling. I doubt this is the only or best method to achieve this and I am open to suggestions...
Now to the problem. If I have one image and one ref, the above process works great, however, once I introduce more images and attempt to get 'current' it fails. But I can console log my ref array and I get everything back including the dimensions of the image.
The problem is staring me in the face but I simply cannot figure out what the problem is. It may simply be that I have misunderstood how references and current work.
Any help would be greatly appreciated.
My code as follows:
import React, { useState, useRef, useEffect, createRef } from "react";
import Images from "../images";
const IllustrationList = () => {
const [images, setImages] = useState([]);
useEffect(() => {
// populate with Images from Images Array
setImages(Images);
}, []);
// ref array
const imageRefs = [];
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (imageRefs.current) {
setDimensions({
width: imageRefs.current.naturalWidth,
height: imageRefs.current.naturalHeight,
});
}
});
return (
<div className="illustration-gallery">
<ul className="cropped-images pl-0">
{images.map((image, i) => {
const newRef = createRef();
imageRefs.push(newRef);
return (
<li className="li-illustration" key={i}>
<a href={image.image} data-lightbox="mygallery">
<img
src={image.image}
alt="illustrations"
ref={newRef}
className={
dimensions.width > dimensions.height ? "imageSize" : ""
}
/>
</a>
</li>
);
})}
</ul>
</div>
);
};
export default IllustrationList;
I was able to resolve my problem.
I think I was completely overthinking the problem and came up with a much simpler solution. I don't know if this is the best way to achieve this, but it does what I want/need it to.
useEffect(() => {
let landScape = document.querySelectorAll("img");
landScape.forEach(() => {
for (var i = 0; i < Images.length; i++) {
if (landScape[i].naturalWidth > landScape[i].naturalHeight) {
landScape[i].className = "landscape";
}
}
});
I'm trying to create a component that tests english vocabulary.
Basically, there are 4 options with 1 correct.
When user chooses option, the right option is highlited in green, and the wrong one in red.
Then user can push the "next" button to go to the next batch of words.
I store refs in object (domRefs, line 68).
Populate it at line 80.
And remove all refs at line 115.
But it doesnt get removed, and leads to error (line 109)
https://stackblitz.com/edit/react-yocysc
So the question is - How to store these refs and what would be the better way to write this component?
Please help, Thanks.
You shouldn't keep refs for component in global variable, since it's making your component singleton. To apply some styles just use conditional rendering instead. Also, it's better to split your test app into several separate components with smaller responsibilities:
const getClassName(index, selected, rightAnswer) {
if (selected === null) {
return;
}
if (index === rightAnswer) {
return classes.rightAnswer;
}
if (index === selected) {
return classes.wrongAnswer;
}
}
const Step = ({ question, answers, rightAnswer, selected, onSelect, onNext }) => (
<div ...>
<div>{ question }</div>
{ answers.map(
(answer, index) => (
<Paper
key={ index }
onClick={ () => onSelect(index) }
className={ getClassName(index, selected, rightAnswer) }
) }
{ selected && <button onClick={ onNext() }>Next</button> }
</div>
);
const Test = () => {
const [ index, setIndex ] = useState();
const word = ..., answers = ..., onSelect = ..., onNext = ...,
return (
<Question
question={ word }
answers={ answers }
... />
);
}