useSWR conditional fetch and react-boostrap Accordion - reactjs

trying to load youtube comments into a infinite load component (using a npm for it)
the mess happens due to the fact the infinite load component is child of parent Accordion component (from react-bootstrap), and what I'm trying to achieve is fetching with useSWR only if Accordion gets clicked (opened).
What I tried is to use useSWR conditional, so that only fetches when state "show" is true, which is being set inside function:
const showComments = () => {
setShow(true)
if (comments) {
setCommChunks(_.chunk(comments.comm, 10))
setCommList(commChunks[counter])
}
}
called on Accordion.Toggle onClick event.
But what happens is I can only show the comments after I click the Accordion twice, why is that?
My code is:
import { useState, useEffect } from 'react'
import { Row, Col, Button, Accordion } from 'react-bootstrap'
import * as _ from 'lodash'
import useSWR from 'swr'
import { MdUnfoldMore } from 'react-icons/md'
import InfiniteScroll from "react-infinite-scroll-component"
import Comments from './Comments'
const siteurl = process.env.NEXT_PUBLIC_SITE_URL
export default function VideoComments({ video }){
const [show, setShow] = useState(false)
const [counter, setCounter] = useState(0)
const [commList, setCommList] = useState(null)
const [commChunks, setCommChunks] = useState([])
const showComments = () => {
setShow(true)
if (comments) {
setCommChunks(_.chunk(comments.comm, 10))
setCommList(commChunks[counter])
}
}
const fetcher = (...args) => fetch(...args).then(res => res.json())
const { data: comments, error } = useSWR(show ? `${siteurl}/api/c/${video.id}` : null, fetcher)
// useEffect(() => {
// if (comments) {
// commChunks = _.chunk(comments.comm, 10)
// setCommList(commChunks[counter])
// }
// },[comments])
const fetchMoreData = () => {
const newCounter = counter + 1;
// loaded all, return
if (commChunks[newCounter] === undefined || commChunks[newCounter] == null) {
return;
}
const newCommList = [
...commList,
...commChunks[newCounter]
]
setCommList(newCommList)
setCounter(newCounter)
}
return (
<div>
<Accordion>
<Row>
<Col xs={12}>
<Accordion.Toggle as={Button} onClick={() => {showComments()}} variant="link" eventKey="0"><div><span>Comments</span></div></Accordion.Toggle>
</Col>
</Row>
<Accordion.Collapse eventKey="0">
<div id="commentsBox" style={{maxHeight: '300px', overflowY: 'auto'}}>
<Col xs={12}>
{commList &&
<InfiniteScroll
dataLength={commList.length}
next={fetchMoreData}
hasMore={true}
scrollableTarget="commentsBox"
>
<Comments data={commList} />
</InfiniteScroll>
}
</Col>
</div>
</Accordion.Collapse>
</Accordion>
</div>
);
}
EDIT: as suggested below I reactivated useEffect, but it still needs two clicks of the Accordion
const showComments = () => {
setShow(true)
if (comments) {
setCommChunks(_.chunk(comments.comm, 10))
setCommList(commChunks[counter])
}
}
const { data: comments } = useSWR(show ? `${siteurl}/api/c/${video.id}` : null, fetcher)
useEffect(() => {
if (comments) {
setCommChunks(_.chunk(comments.comm, 10))
setCommList(commChunks[counter])
}
},[comments])

The issue is in your useEffect, calling setCommList(commChunks[counter]) right after modifying commChunks state won't have the updated value. Setting state in React is an asynchronous operation (see React setState not updating immediately).
You should save the comments in a block-scoped variable and use that to update both states consecutively.
useEffect(() => {
if (comments) {
const commentsChunks = _.chunk(comments.comm, 10)
setCommChunks(commentsChunks)
setCommList(commentsChunks[counter])
}
}, [comments])

You commented the useEffect that handles the comments :
// useEffect(() => {
// if (comments) {
// commChunks = _.chunk(comments.comm, 10)
// setCommList(commChunks[counter])
// }
// },[comments])
What happens :
You click the Accordion, showComments is called
show is set to true, but because comments is undefined, commList and commChunks are not set
the component re-renders, now useSWR can use the url to fetch data
the component re-renders when the fetching si done, now comments contains the data
You click the Accordion the second time, showComments is called
show is set to true, this time commList and commChunks are set
the component re-renders with InfiniteScroll and Comments

Related

Why if statements not working as expected with RTK Query

I'm trying to build an app which pulls data from backend with RTK Query and display that data as posts. I have successfully created RTKQ endpoint that gets data from backend with useEffect. However I want to put data in another array so that I can add more data with infinite scroll to that array but I am not able to do that.
My code is as follow. All help will be highly appreciated.
import { Box, Stack, Skeleton } from "#mui/material";
import React, { useEffect, useState } from "react";
import Post from "../components/postComponent";
import { useGetSimplesMutation } from '../services/authApi'
const Feed = () => {
const [getSimples, { isLoading, isSuccess, data }] = useGetSimplesMutation()
const [loadNow, setLoadNow] = useState(false)
let simplesData = []
useEffect(() => {
const handleGetSimples = async () => {
try {
await getSimples(0).unwrap()
if (isSuccess & !loadNow) {
console.log("Simples loaded")
simplesData = [...simplesData, ...data]
setLoadNow(true)
console.log(simplesData)
}
} catch (error) {
console.error(error.message)
}
}
handleGetSimples()
}, [])
return (
<Box flex={6} p={{ xs: 0, md: 2 }}>
{isLoading && (
<Box marginLeft={3}>
<Stack spacing={1} >
<Skeleton variant="text" height={100} />
<Skeleton variant="text" height={20} />
<Skeleton variant="text" height={20} />
<Skeleton variant="rectangular" height={300} />
</Stack>
</Box>
)}
{(isSuccess && loadNow) && simplesData.map(simples => {
return (
<div key={simples._id}>
<Post
userName={simples.userName}
dateSubmitted={simples.dateSubmitted}
beforePic={simples.beforePic}
afterPic={simples.afterPic}
tag={simples.tag}
/>
</div>
)
})}
</Box>
);
};
export default Feed;
This Code works when I directly map data obtained from RTKQ. However when I pass data to another array in an If statement the code inside if statement does not trigger when isSuccess gets true alongwith loadNow which has default state of false. I want to add data to simplesData array when this condition is true and then setLoadNow to true so that I can render my posts.
The line let simplesData = [] will be re-executed every time that the component re-renders, which will wipe out whatever you have stored.
If you want to keep data across multiple renders then you need to store it in a useState hook.
const [simplesData, setSimplesData] = useState([])
Inside your useEffect, you can call setSimplesData, using a functional update to minimize the dependencies of your effect.
setSimplesData(prevData => [...prevData, ...data])
There are some other issues here, mainly your lack of useEffect dependendencies. I think you want something like this?
const Feed = () => {
const [getSimples, { isLoading, isSuccess }] = useGetSimplesMutation()
const [loadPage, setLoadPage] = useState(0)
const [simplesData, setSimplesData] = useState([])
const handleEndReached = () => {
setLoadPage(prevPage => prevPage + 1);
}
const handleReset = () => {
setSimplesData([]);
setLoadPage(0);
}
useEffect(() => {
const handleGetSimples = async () => {
try {
const pageData = await getSimples(loadPage).unwrap()
console.log(`Simples page ${loadPage} loaded`)
setSimplesData(prevData => [...prevData, ...pageData])
} catch (error) {
console.error(error.message)
}
}
handleGetSimples()
}, [loadPage]);

React: after load page state is true, although useState is false

In the application, by clicking on the button, I want to do 2 things: play an audio file, show 2 pictures. After clicking the button again, I want to turn off the audio and hide 2 pictures. The audio works, but the pictures go crazy. Why? I set useState as "false" in default, but after loading pages they are "true". I do not understand why?
import {useEffect, useRef, useState} from "react";
import styled from "./Cat.module.scss";
import Swing from "../../assets/audio/GirlsLikeToSwing.mp3";
let dancingImg = ['https://i.gifer.com/P6XV.gif', 'https://i.gifer.com/DCy.gif']
const Cat = () => {
const [isPlaying, setIsPlaying] = useState(false);
const audioElement = useRef();
const [ladyDancing, setLadyDancing] = useState(false);
const [catDancing, setCatDancing] = useState(false);
const playPause = () => {
setIsPlaying(!isPlaying);
}
useEffect(() => {
if (isPlaying) {
audioElement.current.play();
setLadyDancing(ladyDancing);
setCatDancing(catDancing);
} else {
audioElement.current.pause();
setLadyDancing(!ladyDancing);
setCatDancing(!catDancing);
}
}, [isPlaying]);
return (<div className={styled.headerContainer}>
<div className={styled.lady}>
{ladyDancing ? <img src={dancingImg[0]} alt="" className={styled.ladyDancing}/> : null}
{catDancing ? <img src={dancingImg[1]} alt="" className={styled.catDancing}/> : null}
</div>
<button onClick={playPause}>Play</button>
<audio src={Swing} ref={audioElement}></audio>
</div>
)
};
export default Cat;
useEffect runs during the first render and all subsequent updates.
I believe you might need to explicitly perform setLadyDancing to true or false depending on the state that you want them to be, otherwise it will keep getting toggled based on its previous state.
Maybe this might work:
useEffect(() => {
if (isPlaying) {
audioElement.current.play();
setLadyDancing(true);
setCatDancing(true);
} else {
audioElement.current.pause();
setLadyDancing(false);
setCatDancing(false);
}
}, [isPlaying]);
Just use isPlaying state for images.
import { useEffect, useRef, useState } from 'react'
import styled from './Cat.module.scss'
import Swing from '../../assets/audio/GirlsLikeToSwing.mp3'
let dancingImg = ['https://i.gifer.com/P6XV.gif', 'https://i.gifer.com/DCy.gif']
const Cat = () => {
const audioElement = useRef()
const [isPlaying, setIsPlaying] = useState(false)
const toggleAudio = () => {
setIsPlaying((prevState) => !prevState)
}
return (
<div className={styled.headerContainer}>
<div className={styled.lady}>
{isPlaying ? <img src={dancingImg[0]} alt="" className={styled.ladyDancing} /> : null}
{isPlaying ? <img src={dancingImg[1]} alt="" className={styled.catDancing} /> : null}
</div>
<button onClick={toggleAudio}>Play</button>
<audio src={Swing} ref={audioElement}></audio>
</div>
)
}
export default Cat
The state is true when the page loads because on the initial render the useEffect block gets called and it's setting them to !ladyDancing and !catDancing which makes it true.
I recommend using a separate function for it (and remove useEffect).
And also about the pictures: you are setting them to their values when isPlaying is true. To fix that, you would need to set them to true (when playing) or false (when not playing).
The code:
const playPause = () => {
setIsPlaying(!isPlaying);
toggle(); // call the function
}
const toggle = () => { // the name of the function can be whatever you want
if (isPlaying) {
audioElement.current.play();
setLadyDancing(true);
setCatDancing(true);
} else {
audioElement.current.pause();
setLadyDancing(false);
setCatDancing(false);
}
}

How to correctly use Hooks in React?

I am new to React, and I have to build a timeout mechanism for a page. I used react-idle-timer, with some help found on the Internet. However, when I try to access the page, I get a Minified React error #321, in which it tells me that I used hooks incorrectly.
Can you please take a look on the following code and point me in the right direction? Thanks
import React from "react"
import NavBar from "./Navbar"
import "../styles/Upload.css"
import LinearProgressWithLabel from "./LinearProgressWithLabel"
import axios from "axios"
import Logout from "./Logout"
import { useIdleTimer } from 'react-idle-timer'
import { format } from 'date-fns'
export default function Upload() {
const [selectedFile, setSelectedFile] = React.useState();
const [progress, setProgress] = React.useState(0);
const timeout = 3000;
const [remaining, setRemaining] = React.useState(timeout);
const [elapsed, setElapsed] = React.useState(0);
const [lastActive, setLastActive] = React.useState(+new Date());
const [isIdle, setIsIdle] = React.useState(false);
const handleOnActive = () => setIsIdle(false);
const handleOnIdle = () => setIsIdle(true);
const {
reset,
pause,
resume,
getRemainingTime,
getLastActiveTime,
getElapsedTime
} = useIdleTimer({
timeout,
onActive: handleOnActive,
onIdle: handleOnIdle
});
const handleReset = () => reset();
const handlePause = () => pause();
const handleResume = () => resume();
React.useEffect(() => {
setRemaining(getRemainingTime())
setLastActive(getLastActiveTime())
setElapsed(getElapsedTime())
setInterval(() => {
setRemaining(getRemainingTime())
setLastActive(getLastActiveTime())
setElapsed(getElapsedTime())
}, 1000)
}, []);
function changeHandler(event) {
setSelectedFile(event.target.files[0])
};
function handleSubmission() {
if (selectedFile) {
var reader = new FileReader()
reader.readAsArrayBuffer(selectedFile);
reader.onload = () => {
sendFileData(selectedFile.name, new Uint8Array(reader.result), 4096)
};
}
};
function sendFileData(name, data, chunkSize) {
function sendChunk(offset) {
var chunk = data.subarray(offset, offset + chunkSize) || ''
var opts = { method: 'POST', body: chunk }
var url = '/api/uploaddb?offset=' + offset + '&name=' + encodeURIComponent(name)
setProgress(offset / data.length * 100)
fetch(url, opts).then(() => {
if (chunk.length > 0) {
sendChunk(offset + chunk.length)
}
else {
axios.post('/api/uploaddb/done', { name })
.then(setProgress(100))
.catch(e => console.log(e));
}
})
}
sendChunk(0);
};
return (
<div>
<NavBar />
<div>
<div>
<h1>Timeout: {timeout}ms</h1>
<h1>Time Remaining: {remaining}</h1>
<h1>Time Elapsed: {elapsed}</h1>
<h1>Last Active: {format(lastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
<h1>Idle: {isIdle.toString()}</h1>
</div>
<div>
<button onClick={handleReset}>RESET</button>
<button onClick={handlePause}>PAUSE</button>
<button onClick={handleResume}>RESUME</button>
</div>
</div>
<h1>Upload</h1>
<input type="file" name="file" onChange={changeHandler} />
{!selectedFile ? <p className="upload--progressBar">Select a file</p> : <LinearProgressWithLabel className="upload--progressBar" variant="determinate" value={progress} />}
<br />
<div>
<button disabled={!selectedFile} onClick={handleSubmission}>Submit</button>
</div>
</div>
)
}
Well, in this case, you should avoid setting states inside the useEffect function, because this causes an infinite loop. Everytime you set a state value, your component is meant to render again, so if you put states setters inside a useEffect function it will cause an infinite loop, because useEffect function executes once before rendering component.
As an alternative you can set your states values outside your useEffect and then put your states inside the useEffect array param. The states inside this array will be "listened" by useEffect, when these states change, useEffect triggers.
Something like this:
React.useEffect(() => {
}, [state1, state2, state3]);
state anti-pattern
You are using a state anti-pattern. Read about Single Source Of Truth in the React Docs.
react-idle-timer provides getRemainingTime, getLastActiveTime and getElapsedTime
They should not be copied to the state of your component
They are not functions
getRemainingTime(), getLastActiveTime(), or getElapsedTime() are incorrect
To fix each:
getRemainingTime should not be stored in state of its own
Remove const [remaining, setRemaining] = useState(timeout)
Remove setRemaining(getRemainingTime) both places in useEffect
Change <h1>Time Remaining: {remaining}</h1>
To <h1>Time Remaining: {getRemainingTime}</h1>
The same is true for lastActive.
getLastActive should be be stored in state of its own
Remove const [lastActive, setLastActive] = React.useState(+new Date())
Remove setLastActive(getLastActiveTime()) both places in useEffect
Change <h1>Last Active: {format(lastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
To <h1>Last Active: {format(getLastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
And the same is true for elapsed.
getElapsedTime should be be stored in state of its own
Remove const [elapsed, setElapsed] = React.useState(+new Date())
Remove setElapsed(getElapsedTime()) both places in useEffect
Change <h1>Time Elapsed: {elapsed}</h1>
To <h1>Time Elapsed: {getElapsedTime}</h1>
remove useEffect
Now your useEffect is empty and it can be removed entirely.
unnecessary function wrappers
useIdleTimer provides reset, pause, and resume. You do not need to redefine what is already defined. This is similar to the anti-pattern above.
Remove const handleReset = () => reset()
Change <button onClick={handleReset}>RESET</button>
To <button onClick={reset}>RESET</button>
Remove const handlePause = () => pause()
Change <button onClick={handlePause}>PAUSE</button>
To <button onClick={pause}>PAUSE</button>
Remove const handleResume = () => resume()
Change <button onClick={handleResume}>RESUME</button>
To <button onClick={resume}>RESUME</button>
avoid local state
timeout should be declared as a prop of the Upload component
Remove const timeout = 3000
Change function Upload() ...
To function Upload({ timeout = 3000 }) ...
To change timeout, you can pass a prop to the component
<Upload timeout={5000} />
<Upload timeout={10000} />
use the provided example
Read Hook Usage in the react-idle-timer docs. Start there and work your way up.
import React from 'react'
import { useIdleTimer } from 'react-idle-timer'
import App from './App'
export default function (props) {
const handleOnIdle = event => {
console.log('user is idle', event)
console.log('last active', getLastActiveTime())
}
const handleOnActive = event => {
console.log('user is active', event)
console.log('time remaining', getRemainingTime())
}
const handleOnAction = event => {
console.log('user did something', event)
}
const { getRemainingTime, getLastActiveTime } = useIdleTimer({
timeout: 1000 * 60 * 15,
onIdle: handleOnIdle,
onActive: handleOnActive,
onAction: handleOnAction,
debounce: 500
})
return (
<div>
{/* your app here */}
</div>
)
}

Increment a part of URL when a button is clicked in ReactJS

I am creating a web app, which is basically an image gallery for a browser game.
The avatars are stored in the game in this format:
https://websitelink.com/avatar/1
https://websitelink.com/avatar/2
https://websitelink.com/avatar/3
So i want to build 2 navigation buttons, one will increment the counter, to move to next image and another one will decrement the counter to move to previous image.
I tried to use props, but since props are immutable it didn't work.
How do I approach building this web app?
Here is the minimal code which may help you to understand about the React Component, props and state.
// parent compoment
import { useState } from "react"
export const GameImageGallery = () => {
const [num, setNum] = useState(0)
const increaseDecrease = (state) => {
if (state === "+") {
setNum(num + 1)
}
if (state === "-") {
setNum(num - 1)
}
}
return (
<>
<button onClick={() => increaseDecrease("-")}>--</button>
<button onClick={() => increaseDecrease("+")}>++</button>
<Image url={`https://websitelink.com/avatar/${num}`} />
</>
)
}
// child component to show image
const Image = ({ url }) => {
return <img src={url} alt="image" />
}
you can do this thing,
const [id,setId]=useState(0);
useEffect(() => {
},[id])
const increment = () => {
setId(id++);
}
const decrement = () => {
setId(id--);
}
return(
<button onClick={increment}>Add</button>
<button onClick={decrement}>remove</button>
<img url={`https://websitelink.com/avatar/${id}`} />
)
useRef is ideal to manage data persistently in a component.
Example:
import { useRef } from 'react'
...
const App = () => {
const links = useRef({url1Ctr : 1})
const onBtnClick = () => {
links.current = { url1Ctr: links.current.url1Ctr + 1}
}
...
}

The React useState() hooks stores undefined always even the data that is to be stored logs correctly using console.log();

Here is the where I am having the problem,
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId);
console.log(videoId);
}
Here when I am trying to log the 'parsedId' it logs the data correctly
ioNng23DkIM
And after using the setVideoId() function when I try to log the value it returns undefined
undefined
Here is a snap shot of the log output.
Home.js code:
import React, { useRef, useState } from "react";
import { Link } from "react-router-dom";
import getYouTubeID from 'get-youtube-id';
function Home(props) {
const [videoLink, setVideoLink] = useState();
const [isBool, setBool] = useState(false);
const [videoId, setVideoId] = useState();
const urlRef = useRef();
const handleChange = (event) => {
setVideoLink(event.target.value);
if (urlRef.current.value === '') {
alert('Please enter a URL');
setBool(true);
} else {
setBool(false);
}
}
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId);
console.log(videoId);
}
return (
<section className="homeLayout">
<div className="logo-display">
<img className="logo-img" alt="logo" src="./logo.png" />
<h1>WatchIt</h1>
</div>
<div className="searchlayer">
<form>
<input ref={urlRef} id="videoLink" placeholder="Enter the youtube video URL:" onBlur={handleChange} required />
<Link style={{ pointerEvents: isBool ? 'none' : 'initial' }} to={`/play?=${videoId}`} onClick={handleCLick}>Play</Link>
</form>
</div>
</section>
);
}
export default Home;
You can use useEffect to solve your problem.
Use effect will listen to you state change n then you can perform logic in there.
The problem you're facing is because setState will set the value eventually, not immediately (Usually this means the update will be visible when the component is rendered again). If you want to do something after the value is set, you need to use useEffect.
Splitting your handleClick we get,
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId); // Queue the change for `videoId`
}
useEffect(() => {
console.log(videoId);
}, [videoId]); // Call this function when the value of `videoId` changes

Resources