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);
}
}
Related
This is the code that I have written to change the background video based on the user input. The background has a video based on the weather of the location the user enters.
import React, { Fragment, useState, useEffect } from "react";
import cloudy from "../assets/cloudy.mp4";
import sunny from "../assets/sunny.mp4";
import rainy from "../assets/rainy.mp4";
import winter from "../assets/winter.mp4";
const weathers = [cloudy, sunny, rainy, winter];
const Background = (props) => {
const [weather, setWeather] = useState(weathers[1]);
const temp = props.info.current.temp_c;
const rain = props.info.current.precip_mm;
if (rain > 2.5) setWeather(weathers[2]);
else if (temp < 8) setWeather(weathers[3]);
return (
<Fragment>
<video autoPlay loop muted className="back-video">
<source src={weather} type="video/mp4" />
</video>
</Fragment>
);
}
Here's how I return in the App component :
return (
<Fragment>
<div className="container">
<h1>Weather App</h1>
<Input newLocation={locationHandler} />
<article>{content}</article>
</div>
<article>{background}</article>
</Fragment>
)
I tried using UseEffect since I have a re-rendering error but in that case it doesn't change the background at all
useEffect(() => {
if (rain > 2.5) setWeather(weathers[2]);
else if (temp < 8) setWeather(weathers[3]);
}, [weather, temp, rain])
or only weather as a dependency.
Edit: My App component
import React, { useState, useEffect, useCallback, Fragment } from
"react";
import Background from "./components/Background";
import Weather from "./components/Weather";
import Input from "./UI/Input";
function App() {
const [weather, setWeather] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [location, newLocation] = useState("New Delhi");
const locationHandler = (place) => {
newLocation(place);
};
const fetchweatherHandler = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
//console.log(location);
const response = await fetch(
`http://api.weatherapi.com/v1/current.json?
key={apiKey}&q=${location}&aqi=yes`
);
if (!response.ok) {
throw new Error("Something went wrong!");
}
const data = await response.json();
//console.log(data);
setWeather(data);
} catch (error) {
setError(error.message);
}
setIsLoading(false);
}, [location]);
useEffect(() => {
fetchweatherHandler();
}, [fetchweatherHandler]);
let content = <p>Found no weather.</p>;
let background = <p>No Background</p>;
if (weather && Object.keys(weather).length > 0) {
content = weather && <Weather info={weather} />;
background = weather && <Background info={weather} />;
}
if (error) {
content = <p>{error}</p>;
}
if (isLoading) {
content = <p>Loading...</p>;
}
return (
<Fragment>
<div className="container">
<h1>Weather App</h1>
<Input newLoc={locationHandler} />
<article>{content}</article>
</div>
<article>{background}</article>
</Fragment>
);
}
export default App;
So UseEffect works like this. Which ever state you are passing in the [], useEffect will listen to its state and whenever the state changes it executes the code inside the useEffect. It also runs the code once the component mounts.
In your case, you are passing [weather] and inside the useEffect you are changing the setWeather which changes weather. So useEffect runs again which set's weather again and it goes on causing an infinite loop and causing too many rerender error.
You can use it like the below but it won't rerender every time weather changes.
useEffect(() => {
if (rain > 2.5) setWeather(weathers[2]);
else if (temp < 8) setWeather(weathers[3]);
}, [temp, rain])
checkout the topic lifecycle of components in react functional components to know more about this
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>
)
}
I am learning React, and trying to build a photo Album with a a modal slider displaying the image clicked (on a different component) in the first place.
To get that, I set <img src={albums[slideIndex].url} /> dynamically and set slideIndex with the idof the imgclicked , so the first image displayed in the modal slider is the one I clicked.
The problem is that before I click in any image albums[slideIndex].urlis obviously undefined and I get a TypeError :cannot read properties of undefined
How could I solve that?
I tried with data checks with ternary operator, like albums ? albums[slideIndex].url : "no data", but doesn't solve it.
Any Ideas? what i am missing?
this is the component where I have the issue:
import React, { useContext, useEffect, useState } from "react";
import { AlbumContext } from "../../context/AlbumContext";
import AlbumImage from "../albumImage/AlbumImage";
import "./album.css";
import BtnSlider from "../carousel/BtnSlider";
function Album() {
const { albums, getData, modal, setModal, clickedImg } =
useContext(AlbumContext);
console.log("clickedImg id >>", clickedImg.id);
useEffect(() => {
getData(); //-> triggers fetch function on render
}, []);
///////////
//* Slider Controls
///////////
const [slideIndex, setSlideIndex] = useState(clickedImg.id);
console.log("SlideINDEx", slideIndex ? slideIndex : "no hay");
const nextSlide = () => {
if (slideIndex !== albums.length) {
setSlideIndex(slideIndex + 1);
} else if (slideIndex === albums.length) {
setSlideIndex(1);
}
console.log("nextSlide");
};
const prevSlide = () => {
console.log("PrevSlide");
};
const handleOnclick = () => {
setModal(false);
console.log(modal);
};
return (
<div className="Album_Wrapper">
<div className={modal ? "modal open" : "modal"}>
<div>
<img src={albums[slideIndex].url} alt="" />
<button className="carousel-close-btn" onClick={handleOnclick}>
close modal
</button>
<BtnSlider moveSlide={nextSlide} direction={"next"} />
<BtnSlider moveSlide={prevSlide} direction={"prev"} />
</div>
</div>
<div className="Album_GridContainer">
{albums &&
albums.map((item, index) => {
return (
<AlbumImage
className="Album_gridImage"
key={index}
image={item}
/>
);
})}
</div>
</div>
);
}
export default Album;
THis is my AlbumContext :
import React, { createContext, useState } from "react";
export const AlbumContext = createContext();
export const AlbumContextProvider = ({ children }) => {
const [albums, setAlbums] = useState();
const [modal, setModal] = useState(false);
const [clickedImg, setClickedImg] = useState("");
const showImg = (img) => {
setClickedImg(img);
setModal(true);
console.log(clickedImg);
};
const getData = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/albums/1/photos"
);
const obj = await response.json();
console.log(obj);
setAlbums(obj);
} catch (error) {
// console.log(error.response.data.error);
console.log(error);
}
};
console.log(`Albums >>>`, albums);
return (
<AlbumContext.Provider
value={{ albums, getData, showImg, modal, setModal, clickedImg }}
>
{children}
</AlbumContext.Provider>
);
};
Thanks very much in advance
Your clickedImg starts out as the empty string:
const [clickedImg, setClickedImg] = useState("");
And in the consumer, you do:
const [slideIndex, setSlideIndex] = useState(clickedImg.id);
So, it takes the value of clickedImg.id on the first render - which is undefined, because strings don't have such properties. As a result, both before and after fetching, slideIndex is undefined, so after fetching:
albums ? albums[slideIndex].url : "no data"
will evaluate to
albums[undefined].url
But albums[undefined] doesn't exist, of course.
You need to figure out what slide index you want to be in state when the fetching finishes - perhaps start it at 0?
const [slideIndex, setSlideIndex] = useState(0);
maybe because your code for checking albums is empty or not is wrong and its always return true condition so change your code to this:
<div className="Album_GridContainer">
{albums.length > 0 &&
albums.map((item, index) => {
return (
<AlbumImage
className="Album_gridImage"
key={index}
image={item}
/>
);
})}
</div>
change albums to albums.length
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
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