react-pdf slow rendering when using scale prop in Page component - javascript

I am making a pdf viewer using react app. I have a pdf with almost 150 pages and I am using zoom-in and zoom-out icons, I am using scale prop in Page component to accomplish that. But whenever I zoom-in or zoom-out there is a slight delay in pdf file to re-render with zoomed pages. And that delay doesnt happen for small pdf.
Here is my code -
const [pages, setPages] = React.useState(0);
const [pagesArr, setPagesArr] = React.useState([]);
const [scrolledValue, setScrolledValue] = React.useState(0);
const [pageNumber, setPageNumber] = React.useState(1);
const [pdfScale, setPdfScale] = React.useState(1.0);
function onLoadSuccess(param) {
setPages(param.numPages);
const numArr = [];
for (let num = 1; num <= param.numPages; num++) {
numArr.push(num);
}
setPagesArr(numArr);
}
function handleZoominIcon() {
setPdfScale(prev => {
if (prev > 2.1) {
return 2.2;
} else {
return prev + 0.1;
}
});
}
function handleZoomoutIcon() {
setPdfScale(prev => {
console.log(prev);
if (prev <= 0.5) {
return 0.5;
} else {
return prev - 0.1;
}
});
}
return (
<section className = 'ppt-view-pdf' style = {scrolledStyle[1], {zoom: '100%'}} ref = {props.pdfViewRef}>
<Document file = {PPT} onLoadSuccess = {onLoadSuccess}>
{pagesArr.map(element => <Page scale = {pdfScale} key = {element} pageNumber = {element}></Page>)}
</Document>
</section>
)
Is there any solution for this?

I had the same issue. The performance was poor because changing the scale factor will re-render all pages. I solved it using react-window that virtualises the invisible components in the dom. I used VariableSizeList because it was more performant than FixedSizeList. Here is a minimal example:
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Document, Page } from "react-pdf/dist/esm/entry.webpack";
import { VariableSizeList } from "react-window";
const PDFDoc = () => {
const [scale, setScale] = useState(1);
const [numPages, setNumPages] = useState(0);
const docRef = useRef<HTMLDivElement>(null);
const [docWidth, setDocWidth] = useState(0);
const [docHeight, setDocHeight] = useState(0);
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
setNumPages(numPages);
const rect = docRef.current?.getBoundingClientRect();
if (rect) {
setDocWidth(rect.width);
setDocHeight(rect.height);
}
};
return (
<div ref={docRef}>
{/* TODO: add your zoom controls here */}
<Document file={"sample.pdf"} onLoadSuccess={onDocumentLoadSuccess}>
<VariableSizeList
width={docWidth}
height={docHeight}
itemCount={numPages}
estimatedItemSize={numPages}
itemSize={() => scale * docWidth}
>
{({ style, index }) => {
const currPage = index + 1;
return (
<div style={style}>
<Page scale={scale} pageNumber={currPage} />
</div>
);
}}
</VariableSizeList>
</Document>
</div>
);
};
You can install react-window here.
Update
Due to some UI issues with react-window, I decided to switch to react-pdf-viewer at the end. It's working pretty well for me so far.

Related

React Scroll position rendering not working

You want to make an event occur when scrolling to that location.
In the current mode, it only works when reloaded at the location, and does not work when it comes to the location while scrolling down from above.
I think I need to use useState, but I tried in many ways but failed. Please help me.
useEffect(() => {
AOS.init();
var cnt = document.querySelectorAll(".count")[props.num];
var water = document.querySelectorAll(".water")[props.num];
const Skills = document.querySelector('#skills');
const percentscroll = window.scrollY + Skills.getBoundingClientRect().top;
if (window.scrollY >= percentscroll) {
var percent = cnt.innerText;
var interval;
interval = setInterval(function () {
percent++;
cnt.innerHTML = percent;
water.style.transform = 'translate(0' + ',' + (100 - percent) + '%)';
if (percent == props.percent) {
clearInterval(interval);
}
}, 60);
}
}, [])
You are not doing it the React way. In React, you use refs to keep tract of DOM nodes. For you case about scrolling to a certain location, you should use IntersectionObserver (mdn). There are many libraries that can help you but I suggest doing it the vanilla way to learn more. Here is an example from dev.to
import { useEffect, useRef, useState } from 'react';
const Header = () => {
const containerRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
const callbackFunction = (entries) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};
const options = {
root: null,
rootMargin: '0px',
threshold: 1
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
if (containerRef.current) observer.observe(containerRef.current);
return () => {
if (containerRef.current) observer.unobserve(containerRef.current);
};
}, [containerRef, options]);
return (
<div>
<div>{isVisible}</div>
<div>A lot of content ...</div>
<div ref={containerRef}>Observe me</div>
</div>
);
};
export default Header;

Array disappears after a few clicks with an undefined error - React

I'm trying to make a simulation of a blackjack hand - first user get two random cards, and with each 'hit' they get another one, however after a few 'hit' the app crashes and the 'undefined' error comes up in (array is undefined therefore can't get length). I've tried to save the deck again in the original shuffle, tried putting it all in one, however I can never get it to fully work.
I suspect it's something to do with useState being used incorrectly, however I'm not sure how to fix it.
Here's my code:
import {useState, useEffect} from 'react'
import Card from '../components/Card';
import Total from '../components/Total';
import {deckArray} from '../utils/data'
export default function Home(){
const initialHand = 2
const [dealCards, setDealCards] = useState(false)
const [isStarted, setIsStarted] = useState(false)
const [isReset, setIsReset] = useState(false)
const [hand, setHand] = useState(initialHand)
const [deck, setDeck] = useState(deckArray)
const [total, setTotal] = useState(0)
const [usersCards, setUsersCards] = useState([])
function shuffle(deck){
console.log("shuffle called")
setIsStarted(true)
let i = deck.length;
while (--i > 0) {
let randIndex = Math.floor(Math.random() * (i + 1));
[deck[randIndex], deck[i]] = [deck[i], deck[randIndex]];
}
setUsersCards(deck.slice(-initialHand))
console.log(usersCards)
console.log(deck)
}
useEffect(() => {
if(dealCards===true){
const randomCard = deck[Math.floor(Math.random()*deck.length)];
const newCardsArray = deck.filter(el => el.index !== randomCard.index)
const chosenCardArray = deck.filter(el => el.index === randomCard.index)
const chosenCard = chosenCardArray[0]
setDeck(newCardsArray)
setUsersCards(prevCards => [...prevCards, chosenCard])
console.log(newCardsArray.length)
setDealCards(false)
}
}, [usersCards, dealCards, deck])
useEffect(() => {
if(isReset){
setUsersCards([])
setDeck(shuffle(deckArray))
setDealCards(false)
setTotal(0)
setIsStarted(true)
}
setIsReset(false)
},[isReset, setIsReset])
useEffect(() => {
if(total>=22){
setIsStarted(true)
setIsReset(true)
setDeck(shuffle(deckArray))
}
}, [total, setTotal])
return (
<>
{isStarted ? <>
<Total usersCards={usersCards} total={total} setTotal={setTotal}/>
<Card usersCards={usersCards} />
<button onClick={() => setDealCards(true)}>HIT</button>
<button>STAND</button>
<button onClick={() => setIsReset(true)}>START OVER</button>
</> :
<>
<p>Game over!</p>
<button onClick={() => shuffle(deck)}>PLAY AGAIN</button></>}
</>
)
}
any help much appreciated!

How to check which functional React Component has been viewed in the viewport latest?

My goal is to make it so I know which video the user has seen in the viewport latest. This was working until I turned the videos into functional React components, which I can't figure out how to check the ref until after the inital render of the React parent. This is currently the top part of the component:
function App() {
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref3 = useRef(null);
function useIsInViewport(ref) {
const [isIntersecting, setIsIntersecting] = useState(false);
const observer = useMemo(
() =>
new IntersectionObserver(([entry]) =>
setIsIntersecting(entry.isIntersecting)
),
[]
);
useEffect(() => {
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref, observer]);
return isIntersecting;
}
var videoProxy = new Proxy(videoViewports, {
set: function (target, key, value) {
// console.log("value " + value)
// console.log("key " + key)
console.log(videoViewports);
if (value) {
setMostRecentVideo(key);
//console.log("Most Rec: " + mostRecentVideo);
}
target[key] = value;
return true;
},
});
const [isGlobalMute, setIsGlobalMute] = useState(true);
const [mostRecentVideo, setMostRecentVideo] = useState("");
videoProxy["Podcast 1"] = useIsInViewport(ref1);
videoProxy["Podcast 2"] = useIsInViewport(ref2);
videoProxy["Podcast 3"] = useIsInViewport(ref3);
And each component looks like this:
<VideoContainer
ref={ref1}
videoProxy={videoProxy}
mostRecentVideo={mostRecentVideo}
setMostRecentVideo={setMostRecentVideo}
title="Podcast 1"
isGlobalMute={isGlobalMute}
setIsGlobalMute={setIsGlobalMute}
videoSource={video1}
podcastName={podcastName}
networkName={networkName}
episodeName={episodeName}
episodeDescription={episodeDescription}
logo={takeLogo}
muteIcon={muteIcon}
unmuteIcon={unmuteIcon}
></VideoContainer>
I had moved the logic for checking if the component was in the viewport into each component, but then it was impossible to check which component was the LATEST to move into viewport. I tried looking online and I don't understand how I would forward a ref here, or how to get the useIsInViewport to only start working after the initial render since it can't be wrapped in a useEffect(() => {}, []) hook. Maybe I'm doing this completely the wrong way with the wrong React Hooks, but I've been bashing my head against this for so long...
First of all: I'm not quite sure, if a Proxy.set is the right way of accomplishing your goal (depends on your overall app architecture). Because setting data does not always mean, the user has really seen the video or is in the viewport.
I've created a simple solution that uses two components. First the a VideoList that contains all videos and manages the viewport calculations so you don't have thousands of event listeners on resize, scroll and so on (or Observers respectively).
The Video component is a forwardRef component, so we get the ref of the rendered HTML video element (or in the case of this example, the encompassing div).
import { forwardRef, useCallback, useEffect, useState, createRef } from "react";
function inViewport(el) {
if (!el) {
return false;
}
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
const Video = forwardRef((props, ref) => {
return (
<div ref={ref}>
<p>{props.source}</p>
<video {...props} />
</div>
);
});
const VideoList = ({ sources }) => {
const sourcesLength = sources.length;
const [refs, setRefs] = useState([]);
useEffect(() => {
// set refs
setRefs((r) =>
Array(sources.length)
.fill()
.map((_, i) => refs[i] || createRef())
);
}, [sourcesLength]);
const isInViewport = useCallback(() => {
// this returns only the first but you can also apply a `filter` instead of the index
const videoIndex = refs.findIndex((ref) => {
return inViewport(ref.current);
});
if (videoIndex < 0) {
return;
}
console.log(`lastSeen video is ${sources[videoIndex]}`);
}, [refs, sources]);
useEffect(() => {
// add more listeners like resize, or use observer
document.addEventListener("scroll", isInViewport);
document.addEventListener("resize", isInViewport);
return () => {
document.removeEventListener("scroll", isInViewport);
document.removeEventListener("resize", isInViewport);
};
}, [isInViewport]);
return (
<div>
{sources.map((source, i) => {
return <Video ref={refs[i]} source={source} key={i} />;
})}
</div>
);
};
export default function App() {
const sources = ["/url/to/video1.mp4", "/url/to/video1.mp4"];
return (
<div className="App">
<VideoList sources={sources} />
</div>
);
}
Working example that should lead you into the right directions: https://codesandbox.io/s/distracted-waterfall-go6g7w?file=/src/App.js:0-1918
Please go over to https://stackoverflow.com/a/54633947/1893976 to see, why I'm using a useState for the ref list.

Import Variable from React Component to Javascript File

I have a react component Button in which I have two states name and players. I want to import these states into another JS file. This file is a vanilla javascript file and not a component.
Here are the codes:
Button.js
import {useState} from "react"
import {buttonContent} from '../buttonContent'
import {correctAnswer} from "../pageTraversal"
import {verifyResults} from "../verifyResults"
const Button = ({textInput, updateApp, updatePage, updateError}) => {
const [pageTitle, updateButton] = useState("home-page")
const [textVisible, textVisibility] = useState("")
const [disabled, changeClick] = useState(false)
const [cursor, changeCursor] = useState("pointer")
const [name, updateName] = useState('')
const [players, updateFriends] = useState('')
const startPages = ["home-page", "welcome", "scenario", "the-adventure-begins"]
const navigatePage = () => {
if (startPages.includes(pageTitle)){
changeCorrectPage()
return
}
if(textInput===""){
updateError("")
return
}
else{
updateError("remove")
}
const response = verifyResults(pageTitle, textInput)
if (pageTitle === "instructions"){
updateName(response["player"])
updateFriends(response["friends"])
changeCorrectPage()
}
if(response === "correct"){
changeCorrectPage()
}
}
const changeCorrectPage = () => {
const page = correctAnswer[pageTitle]
updateApp(page)
updatePage(page)
changeButton(page)
}
const changeButton = (page) => {
textVisibility("hide")
changeClick(true)
changeCursor("auto")
setTimeout(() => {
updateButton(page)
}, 2500)
setTimeout(() => {
textVisibility("show")
}, 4500)
setTimeout(() => {
changeClick(false)
changeCursor("pointer")
}, 6500)
}
return (
<div>
<button className={`button mx-auto`}
id={(pageTitle==="home-page" ? "home-button" : "")}
style={{
"cursor": {cursor},
}}
disabled={disabled}
onClick={navigatePage}
>
<h1 className={`button-text ${textVisible}`}>
{buttonContent[pageTitle]}
</h1>
</button>
<Players name={name} friends={friends} />
</div>
)
}
export default Button
I want to import the two states name and players into another javascript file below where I want to use them as normal variables.
const width = window.innerWidth;
const height = window.innerHeight;
let textSize = ''
if(height > width){
textSize = "small"
}
console.log(name)
console.log(players)
I have tried importing the variables as normal variables, but that doesn't work here. Please suggest a suitable way of doing this.
There is no official approach for this link of problem. But I suggest to use browser storages like localStorage to handle this.
So:
1) Save the states on localStorage based on their changes with useEffect()
useEffect(()=>{
localStorage.setItem("name", name);
localStorage.setItem("players", players);
},[name,players])
2) Get the states data with localStorage everywhere that you want
const width = window.innerWidth;
const height = window.innerHeight;
let textSize = ''
if(height > width){
textSize = "small"
}
console.log(localStorage.getItem("name"))
console.log(localStorage.getItem("players"))

history.push() using react-router-dom works in some components but not others

So as the title says. I'm using React-router-dom and so within my App.js file i have my Router set up containing a Switch and multiple Routes. From a couple of smaller components i have no problem using useHisory and history.push() to manipulate the history and navigate my app.
However within my App.js file it doesn't work and i get returned:
"TypeError: Cannot read property 'push' of undefined"
I'm at a loss as to what is the problem and any help would be much appriciated.
import React, { useState, useEffect } from "react";
import {
BrowserRouter as Router,
Route,
Switch,
useHistory,
} from "react-router-dom";
import styled from "styled-components";
import unsplash from "../api/unsplash";
import Header from "./Header";
import Customise from "./Customise";
import LandingPage from "./LandingPage";
import GameBoard from "./GameBoard";
import GameFinished from "./GameFinished";
function App() {
const [searchImageTerm, setSearchImageTerm] = useState("south africa");
const [images, setImages] = useState([]);
const [randomisedImages, setRandomisedImages] = useState([]);
const [roundStarted, setRoundStarted] = useState(false);
const [firstSelectedTile, setFirstSelectedTile] = useState(null);
const [secondSelectedTile, setSecondSelectedTile] = useState(null);
const [matchedTiles, setMatchedTiles] = useState([]);
const [endOfTurn, setEndOfTurn] = useState(false);
const [score, setScore] = useState(0);
const [minutes, setMinutes] = useState(2);
const [seconds, setSeconds] = useState(0);
const [difficulty, setDifficulty] = useState(8);
const history = useHistory();
useEffect(() => {
getImages();
}, [searchImageTerm, difficulty]);
useEffect(() => {
randomiseImagesWithID(images);
}, [images]);
useEffect(() => {
if (minutes === 0 && seconds === 0) {
finishGame();
}
}, [seconds, minutes]);
const finishGame = () => {
history.push(`/gamefinished`);
};
useEffect(() => {
if (roundStarted) {
let myInterval = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
if (seconds === 0) {
if (minutes === 0) {
clearInterval(myInterval);
} else {
setMinutes(minutes - 1);
setSeconds(59);
}
}
}, 1000);
return () => {
clearInterval(myInterval);
};
}
});
useEffect(() => {
if (matchedTiles.length > 0 && matchedTiles.length === images.length / 2) {
alert("YOU WON!");
}
}, [matchedTiles]);
const getImages = async () => {
const response = await unsplash.get("/search/photos", {
params: { query: searchImageTerm, per_page: difficulty },
});
setImages(response.data.results);
};
const generateTileId = () => {
return "tile_id_" + Math.random().toString().substr(2, 8);
};
const randomiseImagesWithID = (images) => {
let duplicateImagesArray = [...images, ...images];
var m = duplicateImagesArray.length,
t,
i;
while (m) {
i = Math.floor(Math.random() * m--);
t = duplicateImagesArray[m];
duplicateImagesArray[m] = duplicateImagesArray[i];
duplicateImagesArray[i] = t;
}
let finalArray = [];
for (let image of duplicateImagesArray) {
finalArray.push({
...image,
tileId: generateTileId(),
});
}
setRandomisedImages([...finalArray]);
};
const startRound = () => {
setRoundStarted(true);
};
const onTileClick = (tileId, id) => {
// is the tile already paired && is the tile selected && is it the end of the turn?
if (
!matchedTiles.includes(id) &&
tileId !== firstSelectedTile &&
!endOfTurn
) {
// find image id for first selcted id for comparrison
const firstSelctedTileId = randomisedImages.find(
(image) => image.tileId === firstSelectedTile
)?.id;
// if there is no selected tile set first selected tile
if (!firstSelectedTile) {
setFirstSelectedTile(tileId);
} else {
// if the second tile matches the first tile set matched tiles to include
if (id === firstSelctedTileId) {
setMatchedTiles([...matchedTiles, id]);
// add points to score
setScore(score + 6);
// reset selected tiles
setFirstSelectedTile(null);
} else {
// deduct points from score
setScore(score - 2);
// set and display second tile choice
setSecondSelectedTile(tileId);
// set end of turn so tiles cannot be continued to be selected
setEndOfTurn(true);
// reset all values after a few seconds
setTimeout(() => {
setFirstSelectedTile(null);
setSecondSelectedTile(null);
setEndOfTurn(false);
}, 1500);
}
}
}
};
const onResetClick = () => {
randomiseImagesWithID(images);
setFirstSelectedTile(null);
setSecondSelectedTile(null);
setMatchedTiles([]);
setScore(0);
setEndOfTurn(false);
};
return (
<div>
<Router>
<Container>
<Header
onResetClick={onResetClick}
score={score}
seconds={seconds}
minutes={minutes}
/>
<Main>
<Switch>
<Route path="/gameboard">
<GameBoard
images={randomisedImages}
onTileClick={onTileClick}
firstSelectedTile={firstSelectedTile}
secondSelectedTile={secondSelectedTile}
matchedTiles={matchedTiles}
/>
</Route>
<Route path="/customise">
<Customise
setSearchImageTerm={setSearchImageTerm}
setDifficulty={setDifficulty}
setMinutes={setMinutes}
startRound={startRound}
/>
</Route>
<Route path="/gamefinished">
<GameFinished />
</Route>
<Route path="/">
<LandingPage startRound={startRound} />
</Route>
</Switch>
</Main>
</Container>
</Router>
</div>
);
}
export default App;
const Container = styled.div`
width: 100%;
height: 100vh;
display: grid;
grid-template-rows: 7rem;
`;
const Main = styled.div`
display: grid;
grid-template-columns: auto;
`;
And to give an example of where my code is working as expected:
import React from "react";
import { useHistory } from "react-router-dom";
import styled from "styled-components";
function LandingPage({ startRound }) {
const history = useHistory();
const startGame = () => {
history.push(`/gameboard`);
startRound();
};
const customiseGame = () => {
history.push("/customise");
};
return (
<Container>
<WelcomeText>
<p>Match the tiles by picking two at a time.</p>
<p>Gain points for a correct match but lose points if they dont.</p>
<p>Good Luck!</p>
</WelcomeText>
<ButtonContainer>
<GameButton onClick={() => startGame()}>Start</GameButton>
<GameButton onClick={() => customiseGame()}>Customise</GameButton>
</ButtonContainer>
</Container>
);
}
The reason why you are getting TypeError: Cannot read property 'push' of undefined is because you have initialized/assigned history before render has returned (hence the router never populated.
const history = useHistory();
Change it to this and everything should be working as expected (Warning: I haven't tested it myself):
const finishGame = () => {
const history = useHistory();
history.push(`/gamefinished`);
};
It will work because finishGame is only called inside useEffect which is called after the page is rendered.
You can pass history prop one component to another component.
like
// First component
import { useHistory } from "react-router-dom";
const firstComponent = () => {
const history = useHistory();
return (
<SecondComponent history=history />
)
}
const SecondComponent = ({history}) => (
....
);
I don't see any problems with your code. Try this
npm uninstall react-router-dom && npm i react-router-dom
Then try again.

Categories

Resources