Rendering Huge List in React using Intersection Observer - javascript

I am trying to render a huge list of data in React. I know I can use react-window for this usecase but wanted to try if we can implement a similar window based rendering using Intersection Observer API.
I have written this component to try the same. But Here my component is rendering the whole 10,000 divs even if it is not in view port as i am iterating over the data. Is there any way to prevent rendering if the element is not there in viewport similar to react-window. Thank you in advance.
import React from 'react';
import './CustomVirtualizedList.css';
import faker from 'faker';
const generateFakeData = (() => {
const data = [];
for (let i = 0; i < 10000; i++) {
data.push({ id: i, selected: false, label: faker.address.state() })
}
return () => data;
})();
function getRandomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
const ListElement = (props) => {
const [visible, setVisible] = React.useState(false);
const {containerRef} = props;
const elementRef = React.useRef();
let intersectionObserver;
const onVisibilityChange = ([entry]) => {
setVisible(entry.isIntersecting)
}
React.useEffect(() => {
console.log(props);
intersectionObserver = new IntersectionObserver(onVisibilityChange, containerRef.current);
intersectionObserver.observe(elementRef.current);
return () => {
intersectionObserver.disconnect()
}
}, [])
return <div
ref={elementRef}
style={{ backgroundColor: getRandomColor() }}
className="listElement">
{visible ? 'I am visible' : 'I am not visible'}
</div>
}
export const ListContainer = () => {
const containerRef = React.useRef();
const [data, setData] = React.useState(generateFakeData())
return (
<div className="listContainer">
{data.map(val => {
return <ListElement containerRef={containerRef} {...val} />
})}
</div>
);
};

Related

Why Option component re-renders itself after click on any option?

I am making Quiz app in React and I got stuck in a problem where Option component gets re-render itself after clicking each option.
Here is the code
App.js
Main app
export default function App() {
const [questions, setQuestions] = useState([])
const [score, setScore] = useState(0)
// Fetching questions
useEffect(() => {
async function fetchQuestions(){
const res = await fetch("https://opentdb.com/api.php?amount=10&category=18&difficulty=medium")
const data = await res.json()
setQuestions(data.results)
}
fetchQuestions()
}, [])
// Checking answer on clicking any option
const checkAnswer = (option, questionIndex) => {
if(option === questions[questionIndex].correct_answer){
setScore(prevScore => prevScore+=5)
console.log("correct")
}
else{
setScore(prevScore => prevScore-=1)
console.log("incorrect")
}
}
// Main Screen
return (
<QuizScreen questions={questions} score={score} checkAnswer={checkAnswer} />
)
}
QuizScreen.js
Component for rendering quiz screen
export default function QuizScreen(props) {
// Setting questions
const question = props.questions.map((ques, index) => {
// storing options
const opt = []
opt.push(ques.correct_answer)
opt.push(ques.incorrect_answers[0])
ques.incorrect_answers[1] && opt.push(ques.incorrect_answers[1]) // if option 3 available
ques.incorrect_answers[2] && opt.push(ques.incorrect_answers[2]) // if option 4 available
// Arranging options in random order
for(let i=0; i<opt.length; i++){
let j = Math.floor(Math.random() * (i+1))
let temp = opt[i]
opt[i] = opt[j]
opt[j] = temp
}
// Setting options
const option = opt.map(opt => <Option key={nanoid()} option={opt} questionIndex={index} checkAnswer={props.checkAnswer} />)
// Rendering Questions
return (
<div className="ques-container" key={nanoid()}>
<p className="ques-title">{ques.question}</p>
{option}
</div>
)
})
// Main Screen
return (
<div>
<p>{props.score}</p>
{question}
</div>
)
}
Option.js
Component for rendering option buttons
export default function Option(props) {
const [selected, setSelected] = useState(false)
const btnStyle = {
backgroundColor: selected ? "#D6DBF5" : "#FFFFFF"
}
return (
<button
className="ques-option"
onClick={() => {
props.checkAnswer(props.option, props.questionIndex)
setSelected(prevState => !prevState)
}}
style={btnStyle}
>
{props.option}
</button>
)
}
I tried to make Option component separately, but it did not work out
Wrap this around a useMemo
const question = useMemo(() => {
return props.questions.map((ques, index) => {
// storing options
const opt = []
opt.push(ques.correct_answer)
opt.push(ques.incorrect_answers[0])
ques.incorrect_answers[1] && opt.push(ques.incorrect_answers[1]) // if option 3 available
ques.incorrect_answers[2] && opt.push(ques.incorrect_answers[2]) // if option 4 available
// Arranging options in random order
for(let i=0; i<opt.length; i++){
let j = Math.floor(Math.random() * (i+1))
let temp = opt[i]
opt[i] = opt[j]
opt[j] = temp
}
// Setting options
const option = opt.map(opt => <Option key={nanoid()} option={opt} questionIndex={index} checkAnswer={props.checkAnswer} />)
// Rendering Questions
return (
<div className="ques-container" key={nanoid()}>
<p className="ques-title">{ques.question}</p>
{option}
</div>
)
})
}, [props.questions])

Return arrays from custom hook

I created a custom hook that takes a string and 3 arrays of Users, called useSearchUsers
import { useCallback } from 'react';
export default function useSearchUsers() {
const searchUsers = useCallback((searchValue, assistants, interested, rejected) => {
const assistantResults = [];
const interestedResults = [];
const rejectedResults = [];
if (searchValue !== '') {
if (assistants.length > 0) {
assistants.forEach(user => {
if (user.assistantName.includes(searchValue)) {
const name = user.assistantName;
const image = user.assistantImage;
const id = user.assistantId;
assistantResults.push({
id,
name,
image
});
}
});
}
if (interested.length > 0) {
interested.forEach(user => {
if (user.interestedName.includes(searchValue)) {
const name = user.interestedName;
const image = user.interestedImage;
const id = user.interestedId;
interestedResults.push({
id,
name,
image
});
}
});
}
if (rejected.length > 0) {
rejected.forEach(user => {
if (user.rejectedName.includes(searchValue)) {
const name = user.rejectedName;
const image = user.rejectedImage;
const id = user.rejectedId;
rejectedResults.push({
id,
name,
image
});
}
});
}
}
}, []);
return searchUsers;
}
And on the screen where I want to call that hook I have a TextInput where the user can write a string. Here I have declared the assistants, interested and rejected arrays, but for obvious reasons I'm ommiting them here
import useSearchUsers from '../../hooks/assistance/useSearchUsers';
const [eventAssistants, setEventAssistants] = useState([]);
const [eventInterested, setEventInterested] = useState([]);
const [eventRejected, setEventRejected] = useState([]);
const [searchText, setSearchText] = useState('');
const searchUsers = useSearchUsers();
export default function Screen(props) {
return (
<Container>
<SafeAreaProvider>
<TextInput onChangeText={text => setSearchText(text)} />
<Button onPress={() => useSearchUsers(searchText, eventAssistants, eventInterested, eventRejected)} />
</SafeAreaProvider>
</Container>
);
}
My question is, how can I return the 3 results arrays from the hook and passing them to the screen?
You can return an array containing the 3 result arrays.
return [assistantResults, interestedResults, rejectedResults];
instead of return searchUsers;
And to consume it in Screen you can destructure the hook's return value.
const [assistantResults, interestedResults, rejectedResults] = useSearchUsers();

React forwardRef inside a loop

I'm trying to use react forwardRef to call a function inside bunch of child components. Here is the code.
const WorkoutFeedbackForm = ({
latestGameplaySession,
activityFeedbacks,
selectedActivityIndex,
setIsReady,
}) => {
const [isLoading, setIsLoading] = useState(false);
const workoutRef = createRef();
const refMap = new Map();
const onSubmitFeedbackClick = useCallback(async () => {
setIsLoading(true);
await workoutRef.current.onSubmitFeedback();
for (let i = 0; i < activityFeedbacks.length; i++) {
const activityRef = refMap.get(activityFeedbacks[i].sessionID);
console.log(activityRef);
if (activityRef && activityRef.current) {
activityRef.current.onSubmitFeedback();
}
}
setIsLoading(false);
}, [
activityFeedbacks,
refMap,
]);
return (
<>
<FeedbackFormContainer
key={`${latestGameplaySession.id}-form`}
name="Workout Feedback"
feedback={latestGameplaySession.coachFeedback}
isSelected
gameplaySessionDoc={latestGameplaySession}
pathArr={[]}
ref={workoutRef}
/>
{activityFeedbacks.map((feedback, index) => {
const activityRef = createRef();
refMap.set(feedback.sessionID, activityRef);
return (
<FeedbackFormContainer
key={feedback.sessionID}
name={feedback.name}
feedback={feedback.coachFeedback}
isSelected={index === selectedActivityIndex}
gameplaySessionDoc={latestGameplaySession}
pathArr={feedback.pathArr}
setIsReady={setIsReady}
ref={activityRef}
/>
);
})}
<FeedbackSubmit
onClick={onSubmitFeedbackClick}
isLoading={isLoading}
>
Save Feedbacks
</FeedbackSubmit>
</>
);
};
The problem is it seems createRef only works for the component outside the loop. Do you have any idea what's wrong here. Or is it not possible to do that?

How to update styles dynamically in JSX

I am trying to update the individual style of each button when it is clicked, using the useRef() hook from React.
Right now, when I click any button the style change is always applied to the last button rendered.
I believe this is the bit needing attention but I'm stumped.
const handleClick = () => {
status.current.style.background = 'green';
}
Here's the full bit:
import React, { useRef } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
let background = 'blue';
let controls = [];
const makeControls = () => {
for (let i = 1; i <= 9; i++) {
controls.push({active: false});
}
return controls;
};
const ControlPanel = () => {
const status = useRef('blue');
makeControls();
const handleClick = () => {
status.current.style.background = 'green';
}
return (
<>
{controls.map((control, i) => (
<div
ref={status}
style={{background: background}}
className={'box'}
key={i}
onClick={() => handleClick()}></div>
))}
</>
);
};
ReactDOM.render(<ControlPanel />, document.getElementById('root'));
Currently, your ref targets only the last item, you should target all your control items by making an array of refs.
let controls = [];
const makeControls = () => {
for (let i = 1; i <= 9; i++) {
controls.push({ active: false });
}
return controls;
};
makeControls();
const ControlPanel = () => {
const status = useRef([]);
const handleClick = index => {
status.current[index].style.background = 'green';
};
return (
<>
{controls.map((control, i) => (
<div
ref={ref => (status.current[i] = ref)}
style={{ background: `blue`, width: 100, height: 100 }}
key={i}
onClick={() => handleClick(i)}
/>
))}
</>
);
};
When rendering the list of <div>s your status ref is getting reassigned each time, finally stopping on the last element.
which is why the last element gets updated.
Instead why not store the background color info on the control object itself
for (let i = 1; i <= 9; i++) {
controls.push({active: false,background: 'blue'});
}
{controls.map((control, i) => (
<div
style={{background: control.background}}
className={'box'}
key={i}
onClick={() => handleClick(control)}></div>
))}
const handleClick = (control) => {
control.background = 'green';
}
you can use state to do that
like this
import React, { useRef,useState } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
let controls = [];
const makeControls = () => {
for (let i = 1; i <= 9; i++) {
controls.push({active: false});
}
return controls;
};
const ControlPanel = () => {
const [controlState,setControlState]=useState({background:"blue"})
const status = useRef('blue');
makeControls();
const handleClick = () => {
setControlState({background:"green"});
}
return (
<>
{controls.map((control, i) => (
<div
ref={status}
style={{background: controlState.background}}
className={'box'}
key={i}
onClick={() => handleClick()}></div>
))}
</>
);
};
ReactDOM.render(<ControlPanel />, document.getElementById('root'));

How to avoid useRef impacting other elements in the DOM

I was using this code as reference to build a swipeable list of names https://gist.github.com/estaub/91e54880d77a9d6574b829cb0d3ba021
That code becomes a JSX component. I then had a parent component holding two lists, and the idea is to make an item from one of the lists go to the second list when we swipe it.
The code works fine, the problem started when I implemented the lists. When I swipe an Item, the parent component triggers a function that gets the swiped Item, removes from the first and add to the second list. The parent component gets rendered again. The problem is that the item below this one that was erased seems to be receiving the classes that was added to to make the item disappear. In other words, it's like if I was erasing two items. The erased item gets removed from the list, but the item below it still in the DOM now with the properties that hides it, so we can't see it anymore.
For some reason it seems like useRef is selecting the element below the one that has just been erased and applying the styles to it.
Any idea what I could try to solve it?
Here is my swipeable component
import React, {
useEffect, useRef,
} from 'react';
interface IOwnProps {
onSwipe?: () => void;
}
const SwipeableListItem:React.FC<IOwnProps> = ({
children, onSwipe,
}) => {
const listElementRef = useRef<HTMLDivElement | null>(null);
const wrapperRef = useRef<HTMLDivElement | null>(null);
const backgroundRef = useRef<HTMLDivElement | null>(null);
const dragStartXRef = useRef(0);
const leftRef = useRef(0);
const draggedRef = useRef(false);
useEffect(() => {
const onSwiped = () => {
if (onSwipe) {
onSwipe();
}
};
const onDragEnd = () => {
if (draggedRef.current) {
draggedRef.current = false;
const threshold = 0.3;
let elementOffsetWidth = 0;
if (listElementRef.current) {
elementOffsetWidth = listElementRef.current.offsetWidth;
}
if (leftRef.current < elementOffsetWidth * threshold * -1) {
leftRef.current = (-elementOffsetWidth * 2);
if (wrapperRef.current) {
wrapperRef.current.style.maxHeight = (0).toFixed(1);
}
onSwiped();
} else {
leftRef.current = 0;
}
if (listElementRef.current) {
listElementRef.current.className = 'BouncingListItem';
listElementRef.current.style.transform = `translateX(${leftRef.current}px)`;
}
}
};
const onDragEndMouse = (ev: MouseEvent) => {
window.removeEventListener('mousemove', onMouseMove);
onDragEnd();
};
const onDragEndTouch = (ev: TouchEvent) => {
window.removeEventListener('touchmove', onTouchMove);
onDragEnd();
};
window.addEventListener('mouseup', onDragEndMouse);
window.addEventListener('touchend', onDragEndTouch);
return () => {
window.removeEventListener('mouseup', onDragEndMouse);
window.removeEventListener('touchend', onDragEndTouch);
};
}, [onSwipe]);
const onDragStartMouse = (ev: React.MouseEvent) => {
onDragStart(ev.clientX);
window.addEventListener('mousemove', onMouseMove);
};
const onDragStartTouch = (ev: React.TouchEvent) => {
const touch = ev.targetTouches[0];
onDragStart(touch.clientX);
window.addEventListener('touchmove', onTouchMove);
};
const onDragStart = (clientX: number) => {
draggedRef.current = true;
dragStartXRef.current = clientX;
if (listElementRef.current) {
listElementRef.current.className = 'ListItem';
}
requestAnimationFrame(updatePosition);
};
const updatePosition = () => {
if (draggedRef.current) {
requestAnimationFrame(updatePosition);
}
if (listElementRef.current) {
listElementRef.current.style.transform = `translateX(${leftRef.current}px)`;
}
// fade effect
const opacity = (Math.abs(leftRef.current) / 100);
if (opacity < 1 && opacity.toString() !== backgroundRef.current?.style.opacity) {
if (backgroundRef.current) {
backgroundRef.current.style.opacity = opacity.toString();
}
}
};
const onMouseMove = (ev: MouseEvent) => {
const left = ev.clientX - dragStartXRef.current;
if (left < 0) {
leftRef.current = left;
}
};
const onTouchMove = (ev: TouchEvent) => {
const touch = ev.targetTouches[0];
const left = touch.clientX - dragStartXRef.current;
if (left < 0) {
leftRef.current = left;
}
};
return (
<div className="Wrapper" ref={wrapperRef}>
<div className="Background" ref={backgroundRef}>
<span>Remove</span>
</div>
<div
className="ListItem"
ref={listElementRef}
onMouseDown={onDragStartMouse}
onTouchStart={onDragStartTouch}
role="presentation"
>
{children}
</div>
</div>
);
};
export default SwipeableListItem;
Here is the parent component, where I handle the lists
import React, {
useEffect, useState,
} from 'react';
import SwipeableListItem from './SwipeableListItem';
interface ITest{
id:number;
}
interface IOwnProps{
list1?: ITest[];
list2?: ITest[];
}
const MyList: React.FC<IOwnProps> = ({
list1, list2,
}) => {
const [displayList1, setDisplayList1] = useState<boolean>(true);
const [list, setList] = useState<Array<Array<ITest>>>([[], []]);
useEffect(() => {
setList([list1 || [], list2 || []]);
}, [list1, list2]);
const handleSwitchList = () => {
setDisplayList1(!displayList1);
};
//without this function it works, but the item does not get removed from the list
const handleSendToSecondList = (id: number, myList1: ITest[], myList2: ITest[]) => {
const foundItem = myList2.find((s) => s.id === id);
const newList1 = myList1;
if (foundItem) {
// push item to list 1
newList1.push(foundItem);
// remove list 2
const newList2 = myList2.filter((s) => s.id !== foundItem.id);
setList([newList1, newList2]);
}
};
return (
<div>
<div>
<button
type="button"
className={`btn ${displayList1 ? 'btn-primary' : ''}`}
onClick={handleSwitchList}
>
List 1
</button>
<button
type="button"
className={`btn ${!displayList1 ? 'btn-primary' : ''}`}
onClick={handleSwitchList}
>
List 2
</button>
</div>
{
displayList1
? list[1]?.map((item) => (
<SwipeableListItem onSwipe={() => handleSendToSecondList(item.id, list[0], list[1])}>
<p>{item.id}</p>
</SwipeableListItem>
))
: list[0]?.map((item) => (
<SwipeableListItem>
<p>{item.id}</p>
</SwipeableListItem>
))
}
</div>
);
};
export default MyList;
I created this just to show what is happening
https://codesandbox.io/s/react-hooks-usestate-kbvqt?file=/src/index.js
The problem is happening here but a little different. There are two lists, if you swipe an item from the first list, an item from the second list will disappear, because it received the css properties.

Categories