React - assigning ref via loop to image array - arrays

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

Related

React Slider images changing on Click not working (uncaught error too many re-renders)

I'm trying to create a React slider for images. But I'm getting an error that says Uncaught Error: Too many re-renders. If anyone can just point me in the right direction I would really appreciate it. I'm certain that the issue lays within the onClick aspect of the sliderDots mapping.
import React, { useEffect, useState } from 'react';
import Sliderdots from '../CarasouelDots/Sliderdots.component';
import './Slider.styles.scss'
import sliderImages from '../../MockImages/mockimages';
const Slider = () => {
const images = sliderImages;
//Iterator
const [img, setImg] = useState(0);
//Getting all shoe images from an object array
const shoes = images.map(i => (i.shoe));
const heading = images.map(i => (i.title));
const content = images.map(i => (i.content))
const numbers = shoes.map((i, index) => (index))
const indexSet = (number) =>{
setImg(number);
}
//problem with onClick here??
const sliderD = images.map((dot, index) => <Sliderdots key={index} onClick={indexSet(index)}/>);
useEffect(() => {
const timer = setTimeout(() => {
img == shoes.length - 1 ? setImg(0) : setImg(img + 1)
}, 4500)
}, [img]);
return (
<div className='slider-container' style={{ backgroundImage: `url(${shoes[img]})` }}>
<div className='overlay'>
<h1 className='introduction'>{heading[img]}</h1>
<p className='content'>{content[img]}</p>
<div className='dot-container'>
{sliderD}
</div>
</div>
</div>
);
};
export default Slider;
The reason why your component is constantly rerendering is because your onClick property is actually a function call in disguise that gets executed every render:
// This line actually calls the `indexSet` function each time!
const sliderD = images.map((dot, index) => <Sliderdots key={index} onClick={indexSet(index)}/>);
And since indexSet updates the state of the React component by calling setImg, the React component will always end up re-rendering when it reaches that line of code, and since that line of code always re-calls the indexSet function, your component will infinitely re-render.
To fix your code, you just need to replace that onClick property with an anonymous function:
const sliderD = images.map((dot, index) => <Sliderdots key={index} onClick={() => indexSet(index)}/>);

Get the ref of an element rendering by an array

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.

Applying state change to specific index of an array in React

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

fetching or displaying google fonts too slow

so im creating a small webApp using reactjs with material ui,
its basically the settings to customize the color, family, weight of any button or text
so im using the Select to display a list of google font families and when the user picks one it gets added to the overall settings object
when i click on the list it takes a few seconds(~4 to 8 to load Im not sure of the issue nor the solution.
So i was wondering if this is due to my internet connection(3Mbs XD) or might it be something else.
a solution may be setting the list to local storage then it only slow loads once or even have a local version of google fonts...
const [googleFonts, setGoogleFonts] = useState();
useEffect(() => {
let isMounted = true;
let cancelToken = axios.CancelToken.source();
const fetchData = async () => {
try {
let res = await client.get();
if (isMounted) {
setGoogleFonts(res.data.items);
}
} catch (e) {
console.error(e);
};
}
fetchData();
return () => {
isMounted = false;
cancelToken.cancel();
}
}, []);
<Select
labelId="FontFamily"
id="FontFamilySelect"
name="FontFamily"
style={{ width: '188px' }}
defaultValue='PT Sans'
value={widgetFont}
onChange={(e) => setWidgetFont(e.target.value)}
>
{googleFonts?.map((option, index) => (
<MenuItem key={index} value={option.family} >
{option.family}
</MenuItem>
))}
</Select>
Thank you
I don't know about the internet connection, because your user experience could vary. But i think there's some issue in this code.
const [googleFonts, setGoogleFonts] = useState();
You code could break at the beginning, so it's better to anticipate some bad outcome. For example,
const [googleFonts, setGoogleFonts] = useState([]);
Make the initial value an array [] might help in your case.
Or add a spinner if it's not loaded try displaying something else.
The reason this is different than your case is that suppose your loading takes 1s. What the user do around that time? And more importantly your UI has to still functional, ex.
spinner drive user attention somewhere else
empty, so not allow user to do anything, but can't be jammed. Maybe your Select is jammed during transition, i don't know.
Follow this code, your UI render when font loaded
import React, { useEffect, useState } from 'react';
const MyComponent = ( ) => {
const [loading, setLoading] = useState(true);
useEffect( ()=> {
document.fonts.load("20px FontFamilyName").then( () => { setLoading(false) } );
}, [])
return (
<React.Fragment>
{ loading
? <div>Loading...</div>
: <main id="mainWrapper">
Reset of Eelements
</main>
}
</React.Fragment>
);
};
export default MyComponent;

React: How to make collapsible elements that are fast?

I have an element at the top of my page that I want to be collapsible. The trouble is that if there are enough elements below it on the page (about 2000 or more), the act of collapsing/expanding causes the mouse to freeze for a few seconds. How can I add a collapsible element like this and still have a responsive UI?
My methods for collapsing that I have tried are rendering the collapsed element as "null" and rendering with height = 0. Both are slow.
The number of elements following the collapsible element in the example is not that big ~5000 - basically a table with a few hundred rows.
Code sandbox example here: https://codesandbox.io/s/2zi2s
I don't know if this can help. But on my work we implemented a component that can be collapsible with useLayoutEffect.
const InnerCardWrapper: React.FC<IInnerCardWrapper> = ({ isOpen, wrapperCssClasses = '', innerCssClasses = '', children }) => {
const innerCardHeightMeasures = useRef(0);
const [innerCardHeight, setInnerCardHeight] = useState(0);
const elementId = uuid();
useLayoutEffect(() => {
const cardInnerContainer = document.getElementById(elementId);
if (cardInnerContainer) {
innerCardHeightMeasures.current = cardInnerContainer.clientHeight;
}
if (innerCardHeightMeasures.current > 0) {
setInnerCardHeight(innerCardHeightMeasures.current);
}
}, [isOpen]);
useEffect(() => {
setInnerCardHeight(innerCardHeight === 0 ? innerCardHeightMeasures.current : 0);
}, [isOpen]);
return (
<div
style={{ height: innerCardHeight }}
className={`overflow-hidden transition-all ${isOpen ? 'border-b border-gray-light mt-6' : ''} ${wrapperCssClasses}`}
>
<div id={elementId} className={`py-3 ${innerCssClasses}`}>
{children}
</div>
</div>
);
};
export default InnerCardWrapper;
We use TailwindCSS you can check the CSS equivalent here.
Hope this works, please let me know.

Resources