React: How to make collapsible elements that are fast? - reactjs

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.

Related

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

React - assigning ref via loop to image array

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

Images Rerendering inside Styled Component when Chrome Dev Tools is open

This is a bit of a strange one and not sure why it's happening exactly.
When the component mounts, I call a function that in my application makes an HTTP request to get an array of Objects. Then I update 3 states within a map method.
enquiries - Which is just the response from the HTTP request
activeProperty - Which defines which object id is current active
channelDetails - parses some of the response data to be used as a prop to pass down to a child component.
const [enquiries, setEnquiries] = useState({ loading: true });
const [activeProperty, setActiveProperty] = useState();
const [channelDetails, setChannelDetails] = useState([]);
const getChannels = async () => {
// In my actual project,this is an http request and I filter responses
const response = await Enquiries;
const channelDetailsCopy = [...channelDetails];
setEnquiries(
response.map((e, i) => {
const { property } = e;
if (property) {
const { id } = property;
let tempActiveProperty;
if (i === 0 && !activeProperty) {
tempActiveProperty = id;
setActiveProperty(tempActiveProperty);
}
}
channelDetailsCopy.push(getChannelDetails(e));
return e;
})
);
setChannelDetails(channelDetailsCopy);
};
useEffect(() => {
getChannels();
}, []);
Then I return a child component ChannelList that uses styled components to add styles to the element and renders child elements.
const ChannelList = ({ children, listHeight }) => {
const ChannelListDiv = styled.div`
height: ${listHeight};
overflow-y: scroll;
overflow-x: hidden;
`;
return <ChannelListDiv className={"ChannelList"}>{children}</ChannelListDiv>;
};
Inside ChannelList component I map over the enquiries state and render the ChannelListItem component which has an assigned key on the index of the object within the array, and accepts the channelDetails state and an onClick handler.
return (
<>
{enquiries &&
enquiries.length > 0 &&
!enquiries.loading &&
channelDetails.length > 0 ? (
<ChannelList listHeight={"380px"}>
{enquiries.map((enquiry, i) => {
return (
<ChannelListItem
key={i}
details={channelDetails[i]}
activeProperty={activeProperty}
setActiveProperty={id => setActiveProperty(id)}
/>
);
})}
</ChannelList>
) : (
"loading..."
)}
</>
);
In the ChannelListItem component I render two images from the details prop based on the channelDetails state
const ChannelListItem = ({ details, setActiveProperty, activeProperty }) => {
const handleClick = () => {
setActiveProperty(details.propId);
};
return (
<div onClick={() => handleClick()} className={`ChannelListItem`}>
<div className={"ChannelListItemAvatarHeads"}>
<div
className={
"ChannelListItemAvatarHeads-prop ChannelListItemAvatarHead"
}
style={{
backgroundSize: "cover",
backgroundImage: `url(${details.propertyImage})`
}}
/>
<div
className={
"ChannelListItemAvatarHeads-agent ChannelListItemAvatarHead"
}
style={{
backgroundSize: "cover",
backgroundImage: `url(${details.receiverLogo})`
}}
/>
</div>
{activeProperty === details.propId ? <div>active</div> : null}
</div>
);
};
Now, the issue comes whenever the chrome dev tools window is open and you click on the different ChannelListItems the images blink/rerender. I had thought that the diff algorithm would have kicked in here and not rerendered the images as they are the same images?
But it seems that styled-components adds a new class every time you click on a ChannelListItem, so it rerenders the image. But ONLY when the develop tools window is open?
Why is this? Is there a way around this?
I can use inline styles instead of styled-components and it works as expected, though I wanted to see if there was a way around this without removing styled-components
I have a CODESANDBOX to check for yourselves
If you re-activate cache in devtool on network tab the issue disappear.
So the question becomes why the browser refetch the image when cache is disabled ;)
It is simply because the dom change so browser re-render it as you mentioned it the class change.
So the class change because the componetn change.
You create a new component at every render.
A simple fix:
import React from "react";
import styled from "styled-components";
const ChannelListDiv = styled.div`
height: ${props => props.listHeight};
overflow-y: scroll;
overflow-x: hidden;
`;
const ChannelList = ({ children, listHeight }) => {
return <ChannelListDiv listHeight={listHeight} className={"ChannelList"}>{children}</ChannelListDiv>;
};
export default ChannelList;
I think it has to do with this setting to disable cache (see red marking in image)
Hope this helps.

React: Get a list of tags by attribute values

I am trying to make a text which displays some information upon mouse hover. For example, I have three tags with following information
<div class="body main-seq" style="display: inline;">
<span prob="67.8">
Foo
</span>
<span prob="67.8;34.6">
Bar
</span>
<span prob="67.8;34.6;52.7">
Hello
</span>
</div>
On a browser, it will look something like this
FooBarHello
Basically, when user hovers a mouse on first bit of the text (the one that corresponds to "Bar"), I want to bold all the span tags that contain "34.6" in its "prob" attribute. In this case, it would have to bold "BarHello", but leave "Foo" as it is.
After doing some Google search, this task seems pretty trivial in Javascript or jQuery, and can be done by doing something like so,
$("span[prob*='34.6']") (along with onMouseOver event or something similar)
Find an element in DOM based on an attribute value
However, I've seen many posts saying I should absolutely try to avoid using jQuery in React because React and jQuery has conflicting philosophy (React renders DOM every time the data changes whereas jQuery directly manipulates DOM). Please correct me if I am wrong though.
So my question is, how can I achieve this in React?
You could perhaps do something like this in React:
import React, { useState } from 'react';
function MyComponent(props) {
const [ isHovered, setIsHovered ] = useState(false);
const onMouseEnter = () => {
setIsHovered(true);
}
const onMouseLeave = () => {
setIsHovered(false);
}
const spans = [
{ text: 'Foo', probs: [67.8] },
{ text: 'Bar', probs: [67.8, 34.6] },
{ text: 'Hello', probs: [67.8, 34.6, 52.7] }
];
return (
<div
class="body main-seq"
style="display: inline;"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{spans.map(({ text, probs }) => {
const isBold = isHovered && probs.includes(34.6);
return (
<span style={{ fontWeight: isBold ? 'bold' : 'normal'; }}>
{text}
</span>
);
}}
</div>
);
}
I hope this helps.
I think you're approaching this the wrong way, but it's a great exercise in "thinking in React".
You basically have these span tags that store number values. Each of them have the same responsibilities:
Store a list of number values
When clicked, set the new "criteria" values
let the others know that they should check if they should be bold or not
So let's call this component SpanComponent. We want a structure like this:
export const SpanComponent = ({ values, activeValues, setActiveValues, children }) => {
const isBold = checkForMatchingValues(values, activeValues) // returns true or false
const onMouseEnter = event => {
setActiveValues(values)
}
return (
<span style={{ fontWeight: isBold ? 'bold' : 'normal' }} onMouseEnter={onMouseEnter}>{children}</span>
)
}
Then in our main component we can manage these as so:
export const MainComponent = () => {
const [activeValues, setActiveValues] = useState([]) // empty array as default value
return (
<SpanComponent values={[68.8]} activeValues={activeValues} setActiveValues={setActiveValues}/>
<SpanComponent values={[68.8, 34.6]} activeValues={activeValues} setActiveValues={setActiveValues}/>
<SpanComponent values={[68.8, 34.6, 52.7]} activeValues={activeValues} setActiveValues={setActiveValues}/>
)
}

Resources