I'm building a portfolio app in React JS and one of my pages is an About Me page. I stumbled upon a youtube video that builds an infinite carousel using vanilla JavaScript, and during my initial testing it worked. However, when I navigate away from my 'About Me' page and return, it explodes with a "TypeError: Cannot read property 'style' of null" within my About Me component "stepNext; src/components/about-me/AboutMe.js:34".
import React from "react";
import "./AboutMe.css"
import { Button,
Fade,
Grow,
Typography } from '#material-ui/core'
import { ArrowBackIos, ArrowForwardIos } from "#material-ui/icons";
import { Background, Adventures, Hobbies } from "./about-me-components/index";
export const AboutMe = () => {
const slider = document.querySelector('.slider-about-me')
const carousel = document.querySelector('.carousel-about-me')
let direction = 1
const stepPrevious = () => {
if (direction === 1) {
slider.appendChild(slider.firstElementChild)
}
direction = -1
console.log("Previous", direction)
carousel.style.justifyContent = 'flex-end'
slider.style.transform = 'translate(33%)'
}
const stepNext = () => {
if (direction === -1) {
slider.prepend(slider.lastElementChild)
}
direction = 1
console.log("Next", direction)
carousel.style.justifyContent = 'flex-start'
slider.style.transform = 'translate(-33%)'
}
const sliderAppend = () => {
if (direction === 1) {
slider.appendChild(slider.firstElementChild)
} else if (direction === -1) {
slider.prepend(slider.lastElementChild)
}
slider.style.transition = 'none'
slider.style.transform = 'translate(0)'
setTimeout(() => {slider.style.transition = 'all 0.5s'})
}
return (
<>
<Fade
in={true}
timeout={1500}
>
<div
id='about-me-container'
>
<div className="controls">
<div
className='arrow-span-about-me arrow-left-about-me'
>
<Button
className='button-about-me arrow-about-me'
variant='contained'
onClick={stepPrevious}
>
<ArrowBackIos
className="arrow-back-about-me"
/>
</Button>
</div>
<div
className='arrow-span-about-me arrow-right-about-me'
>
<Button
className='button-about-me arrow-about-me'
variant='contained'
onClick={stepNext}
>
<ArrowForwardIos
className="arrow-forward-about-me"
/>
</Button>
</div>
</div>
<div
id="about-me-carousel-container"
>
<div
className='carousel-about-me'
>
<div
className='slider-about-me'
onTransitionEnd={sliderAppend}
>
<section className='text-white'><Background /></section>
<section className='text-white'><Adventures /></section>
<section className='text-white'><Hobbies /></section>
</div>
</div>
</div>
</div>
</Fade>
</>
)
}
The only reason I chose this route is because I haven't been able to find a half decent infinite carousel module with easy customization abilities. As much as I would prefer for this to work, I'm open to suggestions and/or solutions. Much appreciated!
I would suggest using useRef instead of document.querySelector
document.querySelector happens outside of that lifecycle, making what it returns unreliable, while refs happen within it. (Though doesn’t get reset because of a lifecycle event like a re-render.) This ensures the object returned by the ref is an accurate representation of the current state of the virtual DOM.
I think this is the reason why you are encountering the said error when you go away and back from the About Page.
Here's an example:
https://codesandbox.io/s/objective-fast-vhc27?file=/src/modalAndButton.jsx:531-648
Related
import { useState } from 'react'
import styles from '../styles/Login.module.css'
import {motion as m} from 'framer-motion'
const words = ['Sheetal','Jackie','Rohan','Narayan','Budwiser']
export default function Login() {
const [step,setStep] = useState(0)
const initial = {
transform:'translateX(100%)'
}
const animate = {
transform:'translateX(0)'
}
return (<main>
<div className={styles.carousel_parent}>
<div className={styles.track}>
{
<m.div initial={initial} animate={animate} className={styles.slides}><div>{words[step]}</div></m.div>
}
</div>
<div className={styles.controls}>
<button
disabled={step<1?true:false}
onClick={()=>{
const total = words.length;
if(step - 1 < 0){
return false
}else{
setStep(step-1)
}
}}>Prev</button>
<div className={styles.dots}>
</div>
<button
disabled={step+1>words.length-1?true:false}
onClick={()=>{
const total = words.length;
if(step + 1 > words.length-1){
return false
}else{
setStep(step+1)
}
}}>Next</button>
</div>
</div>
</main>)
}
The above is my code. What I want to acheive is when someone clicks on the next button, the slide should change with transition. I have acheived everything however the transition effect using framer motion is not working.
**What have I tried using : **
I have tried prefixing 'm.' in litrally every component but nothings seems to work.
I was trying to make a react slider component but for some reasons it doesn't change/update the image on click instead it always displays the same picture. What i'm trying to achieve is when I click the button it updates index and the display image changes according to index position. Even though the index is updating the image is not changing. My attempt at creating the slider component -
import arrow from '../../img/arrow-outlined-crimson.png';
const Slider = ({ images }) => {
let index = 0;
const nextSlide = () => {
index+=1;
if (index > images.length - 1) {
index = 0
}
}
return (
<div className='slider'>
<div ref={sliderRef} className="wrapper">
<img src={images[index]} alt="" className="sliderImg" />
</div>
<img src={arrow} onClick={nextSlide} alt="Arrow" className="sliderIcon iconRight" />
</div>
)
}
export default Slider
Use React State instead of just variables.
Try this:
const [index, setIndex] = React.useState(0)
const nextSlide = () => {
setIndex(index + 1);
if (index > images.length - 1) {
setIndex(1)
}
}
I'm currently learning ReactJS.
I created a simple application and I got two buttons.
On a desktop everything works fine and smooth.
On my iPhone using Safari however, when I click the button there is a delay before the onClick action changes appear.
I read about the 300ms delay on mobile devices but none of the solutions given worked for me. Ref: https://www.sitepoint.com/5-ways-prevent-300ms-click-delay-mobile-devices/
import React, { useState } from "react";
import './form_donation.scss';
function FormDonationAmount() {
const [amount, setAmount] = useState(0);
const [amountText, setAmountText] = useState(amount.toString());
function updateAmount(amount: number) {
if (amount > 99999) {
setAmount(99999);
} else if (amount < 0) {
setAmount(0);
} else {
setAmount(amount);
}
setAmountText(amount.toString());
}
function handleAmountChange(e: any) {
var parsed = parseInt(e.target.value);
if (isNaN(parsed)) {
setAmountText("");
updateAmount(0);
return;
}
updateAmount(parsed);
}
return (
<div className="inputs">
<div id="amount_input">
<input type="number" onChange={handleAmountChange} value={amountText} />
<button onClick={() => updateAmount(amount - 10)}>-</button>
<button onClick={() => updateAmount(amount + 10)}>+</button>
</div>
</div>
);
}
export default FormDonationAmount;
This is very frustrating for the user experience. Any ideas how I could fix this ?
Thanks
As it turns out, it was my css that was absolutely massive for the mobile to handle. So yeah, big moving blurry things in the background are cool but not very optimised...
I've built a grid of images coming from an API, and now I am trying to build a slider.
First image to be displayed should be the one clicked in the Grid below the slider , and when clicked next/prev go to the nex/prev img in the array.
How could I get it?
I am trying by setting the initial state of ``ìmgToShow```to the img clicked, but i get nothing in console. The idea would be to get that, and then , i still dont know how, update the state to show the next img in the Array.
album is the array of objects with all the images. Fetched in my Context component
clickedImagecontains the clicked image's URL. I get it from a click event in other component.
This is my component :
import React, { useContext, useState } from "react";
import { AlbumContext } from "../../context/AlbumContext";
import "./carousel.css";
import BtnSlider from "./BtnSlider";
function Carousel() {
const { clickedImage, albums } = useContext(AlbumContext);
const [imgToShow, setImgToShow] = useState(clickedImage);
console.log("imgTOSHOW", imgToShow); // empty in console
console.log("clickedIMG in context", clickedImage); // gives me the URL clicked
const [slideIndex, setSlideIndex] = useState(1);
const nextSlide = () => {
if (slideIndex !== albums.length) {
setSlideIndex(slideIndex + 1);
} else if (slideIndex === albums.length) {
setSlideIndex(1);
}
};
const prevSlide = () => {};
return (
<div className="container-slider">
{albums &&
albums.map((item, index) => {
return (
<div
key={item.id}
className={
slideIndex === index + 1 ? "slide active-anim" : "slide"
}
>
<img src={imgToShow} alt="" />
</div>
);
})}
<BtnSlider moveSlide={nextSlide} direction={"next"} />
<BtnSlider moveSlide={prevSlide} direction={"prev"} />
</div>
);
}
export default Carousel
I'm having a problem with inline style changes that are reset when my dispatch is finished, because the state is being re-rendered, despite the other functionality of my component is working (you can still see that the counter is not stopping).
Here's a demonstration of what I mean.
You can see that the orange bar of the left box vanishes when the orange bar of the right bar finishes (the animation ends). Essentially what I'm doing here is changing the width property in inline styles.
import React, { useEffect, useRef } from "react";
import { useDispatch, connect } from "react-redux";
import { addProfessionExperience } from "../../actions/index";
import "./Professions.sass";
const timers = [];
const progressWidths = [];
const mapStateToProps = (state, ownProps) => {
const filterID = +ownProps.match.params.filter;
const { professions, professionExperience } = state;
return {
professions: professions.find(item => item.id === filterID),
professionExperience: professionExperience
};
};
const produceResource = (dispatch, profession, sub, subRef) => {
if(timers[sub.id]) return;
/*
* Begin the progress bar animation/width-change.
*/
Object.assign(subRef.current[sub.id].style, {
width: "100%",
transitionDuration: `${sub.duration}s`
});
/*
* Updates the progress text with the remaining time left until done.
*/
let timeLeft = sub.duration;
const timeLeftCountdown = _ => {
timeLeft--;
timeLeft > 0 ? setTimeout(timeLeftCountdown, 1000) : timeLeft = sub.duration;
subRef.current[sub.id].parentElement.setAttribute("data-duration", timeLeft + "s");
}
setTimeout(timeLeftCountdown, 1000);
/*
* Dispatch the added experience from profession ID and sub-profession level.
* We do not allow duplicate timers, only one can be run at a time.
*/
const timer = setTimeout(() => {
Object.assign(subRef.current[sub.id].style, {
width: "0%",
transitionDuration: "0.2s"
});
dispatch(addProfessionExperience({ id: profession.id, level: sub.level }));
delete timers[sub.id];
}, sub.duration * 1000);
timers[sub.id] = timer;
};
const isSubUnlocked = (professionMaxExperience, subLevel, professionExperience) => {
if(professionExperience <= 0 && subLevel > 1) return false;
return professionExperience >= getExperienceThreshold(professionMaxExperience, subLevel);
};
const getExperienceThreshold = (professionMaxExperience, subLevel) => (((subLevel - 1) * 1) * (professionMaxExperience / 10) * subLevel);
const ConnectedList = ({ professions, professionExperience }) => {
const currentExperience = professionExperience.find(item => item.profession === professions.id);
const subRef = useRef([]);
const dispatch = useDispatch();
useEffect(() => {
subRef.current = subRef.current.slice(0, professions.subProfessions.length);
}, [professions.subProfessions]);
return (
<div>
<div className="list">
<ul>
{professions.subProfessions.map(el => {
const unlocked = isSubUnlocked(
professions.maxExperience,
el.level,
(currentExperience ? currentExperience.amount : 0)
);
const remainingExperience = getExperienceThreshold(professions.maxExperience, el.level) - (currentExperience ? currentExperience.amount : 0);
return (
<li
key={Math.random()}
style={{ "opacity": unlocked ? "1" : "0.5" }}
>
<div className="sprite">
<img alt="" src={`/images/professions/${el.image}.png`} />
</div>
<div className="caption">{el.name}</div>
<div
className="progress-bar"
data-duration={unlocked ? `${el.duration}s` : `${remainingExperience} XP to Unlock`}
data-identifier={el.id}
>
<span ref={r => subRef.current[el.id] = r} ></span>
</div>
<div className="footer">
<button
className="btn"
onClick={() => unlocked ? produceResource(dispatch, professions, el, subRef) : false}
>
{unlocked ?
`Click` :
<i className="fa fa-lock"></i>
}
</button>
</div>
</li>
);
})}
</ul>
</div>
</div>
);
};
const List = connect(mapStateToProps)(ConnectedList);
export default List;
How can I make it so the orange bars persist on their own and not disappears when another one finishes?
One problem is that you're using Math.random() to generate your keys. Keys are what the virtual DOM uses to determine whether an element is the "same" as the one on a previous render. By using a random key, you're telling the virtual DOM that you want to spit out a brand new DOM element instead of reusing the prior one, which means the new one won't retain any of the side effects you placed on the original element. Read up on React's reconciliation for more info on this.
Try to use keys that logically represent the thing you're rendering. In the case of your code, el.id looks like it may be a unique identifier for the subprofession you're rendering. Use that for the key instead of Math.random().
Additionally, refs are going to make reasoning about your code really difficult. Rather than using refs to manipulate your DOM, use state manipulation and prop passing, and let React re-render your elements with the new attributes.