How to map out preloaded images from an array in react? - javascript

I am trying to preload some images for an image carousel and store them in an array. I seem to have everything working so far except when I try to map the images in the array in to JSX I get an error.
Error: Objects are not valid as a React child (found: [object HTMLImageElement]). If you meant to render a collection of children, use an array instead
Can someone tell me why please?
As a follow up question, my setInterval (which will be used to rotate through the images) isn't starting and I can't work out why so any help with that would be greatly appreciated.
import React, { useEffect, useState } from 'react'
import { CSSTransition } from 'react-transition-group'
import { ImageCarouselContainer, ImageCarouselSlide } from './imagecarousel.styles'
const images = [
'https://images.unsplash.com/photo-1588392382834-a891154bca4d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2555&q=80',
'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2551&q=80',
'https://images.unsplash.com/photo-1470813740244-df37b8c1edcb?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2551&q=80'
]
const ImageCarousel = () => {
const [activeImage, setActiveImage] = useState(1);
const [imagesArr, setImagesArr] = useState([])
useEffect(() => {
let loadedImages = []
images.forEach(el => {
let img = new Image()
img.onload = () => {
loadedImages.push(img);
}
img.src = el
})
setImagesArr(loadedImages);
const counter = () => {
if(activeImage < imagesArr.length) {
setActiveImage(activeImage + 1)
} else {
setActiveImage(0)
}
}
const interval = setInterval(counter, 1000)
return () => {
clearInterval(interval);
}
}, [])
return (
<ImageCarouselContainer>
{
imagesArr &&
imagesArr.map((el, idx) => (
<CSSTransition
classNames='image'
timeout={1000}
key={idx}
in={activeImage === idx ? true : false}
unmountOnExit
>
<ImageCarouselSlide
>
{el}
</ImageCarouselSlide>
</CSSTransition>
))
}
</ImageCarouselContainer>
)
}
export default ImageCarousel

you try to put pure html object not react component to render function. so it dosn't has props etc...
change
images.forEach(el => {
let img = new Image()
img.onload = () => {
loadedImages.push(img);
}
img.src = el
}
to
images.forEach(el => {
let img = new Image()
img.onload = () => {
loadedImages.push(<img src={el}>);
}
img.src = el
}

Related

How to clear setTimeout when the page is changed in reactjs - nextjs

I created a slideshow in the Nextjs project, But I have a bug. When the user clicks on a link and the page has changed I get an Unhandled Runtime Error and I know it because of the setTimeout function it calls a function and tries to style an element that does not exist on the new page.
How can I clear the setTimeout function after the user click the links?
Error screenshot:
My component code:
import { useEffect, useState } from "react";
import SlideContent from "./slide-content";
import SlideDots from "./slide-dots";
import SlideItem from "./slide-item";
const Slide = (props) => {
const { slides } = props;
const [slideLength, setSlideLength] = useState(slides ? slides.length : 0);
const [slideCounter, setSlideCounter] = useState(1);
const handleSlideShow = () => {
if (slideCounter < slideLength) {
document.querySelector(
`.slide-content:nth-of-type(${slideCounter})`
).style.left = "100%";
const setSlide = slideCounter + 1;
setSlideCounter(setSlide);
setTimeout(() => {
document.querySelector(
`.slide-content:nth-of-type(${setSlide})`
).style.left = 0;
}, 250);
} else {
document.querySelector(
`.slide-content:nth-of-type(${slideCounter})`
).style.left = "100%";
setSlideCounter(1);
setTimeout(() => {
document.querySelector(`.slide-content:nth-of-type(1)`).style.left = 0;
}, 250);
}
};
useEffect(() => {
if (slideLength > 0) {
setTimeout(() => {
handleSlideShow();
}, 5000);
}
}, [slideCounter, setSlideCounter]);
return (
<>
<div className="slide-button-arrow slide-next">
<span className="carousel-control-prev-icon"></span>
</div>
<div className="slide">
{slides.map((slide) => (
<SlideContent key={`slide-${slide.id}`}>
<SlideItem img={slide.img} title={slide.title} />
</SlideContent>
))}
<SlideDots activeDot={slideCounter} totalDots={slides} />
</div>
<div className="slide-button-arrow slide-prev">
<span className="carousel-control-next-icon"></span>
</div>
</>
);
};
export default Slide;
I use my slideshow component inside the home page file.
useEffect(() => {
let timer;
if (slideLength > 0) {
timer=setTimeout(() => {
handleSlideShow();
}, 5000);
}
return () => {
clearTimeout(timer);
};
}, [slideCounter, setSlideCounter]);
you should remove your timeout function when the component unmounts.
(if you're using old syntax there is componentWillUnmount() function)
when you are using hooks you can return your useEffect so it will cause the unmount function.
in your case it will be something like this:
useEffect(() => {
//define a temp for your timeout to clear it later
let myTimeout;
if (slideLength > 0) {
//assign timeout function to the variable
myTimeout = setTimeout(() => {
handleSlideShow();
}, 5000);
}
// this triggers when the component unmounts or gets re-rendered.
// you can clear the timeout here.
return () => {
clearTimeout(myTimeout);
}
}, [slideCounter, setSlideCounter]);
you should always remove your timeouts because you don't want memory leaks and performance issues. it might not give you errors but clear them all.
there is an old post i guess you can read here

LocalStorage does not save after page refresh (React context)

I am developing a context in which through a function I can send "pokemons" to a global array, and also send the information of this array to my localstorage so that it is saved in the browser, I managed to do that and the array items are in localstorage, but every time the site refreshes, localstorage goes back to the empty array.
import React, { useEffect, useState } from "react";
import CatchContext from "./Context";
const CatchProvider = ({ children }) => {
const [pokemons, setPokemons] = useState([], () => {
const dataStorage = localStorage.getItem('pokemons');
if (dataStorage) {
return JSON.parse(dataStorage)
} else {
return [];
}
});
useEffect(() => {
localStorage.setItem('pokemons', JSON.stringify(pokemons));
}, [pokemons]);
const updatePokemons = (name) => {
const updatedPokemons = [...pokemons];
const pokemonsIndex = pokemons.indexOf(name);
if (pokemonsIndex >= 0) {
updatedPokemons.slice(pokemonsIndex, 1)
} else {
updatedPokemons.push(name)
};
setPokemons(updatedPokemons)
}
const deletePokemon = async (name) => {
await pokemons.splice(pokemons.indexOf(toString(name)))
}
return (
<CatchContext.Provider value={{ pokemons: pokemons, updatePokemons: updatePokemons, deletePokemon: deletePokemon }}>
{children}
</CatchContext.Provider>
);
}
export default CatchProvider;
The problem is that useState doesn't take two arguments.
Instead of:
const [pokemons, setPokemons] = useState([], () => {
You want:
const [pokemons, setPokemons] = useState(() => {
I think you don't need to call useEffect on initial render so you can make use of refs for this
import { useEffect, useRef } from "react";
// other code....
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) {
localStorage.setItem('pokemons', JSON.stringify(pokemons));
} else {
didMount.current = true;
}
}, [pokemons]);

How can I change the onClick function of a button saved in a useRef object in a react component?

I'm learning state and hooks in React. I'm doing an exercise to deal cards from a deck using the deckofcardsAPI, not the most important code on the interwebs, but it does bring about a question. In fact, it's not even part of the exercise, I just really want to do it.
I have a button that is stored in a useRef object. I really would like the onClick function to change, but I'm not sure how to do it.
import React, { useState, useEffect, useRef } from "react";
import axios from 'axios';
// auto dealer, 1 card/s turned on or off
function CardTable2() {
const [src, setSrc] = useState('');
const deckId = useRef();
const remaining = useRef(52);
const isDealing = useRef(false);
const timerId = useRef();
const button = useRef();
useEffect(() => {
timerId.current = setInterval(() => {
if (isDealing.current) {
dealCard();
};
}, 1000);
async function createDeck() {
const res = await axios.get(`http://deckofcardsapi.com/api/deck/new/shuffle/?deck_count=1`);
deckId.current = res.data.deck_id;
};
createDeck();
return () => { clearInterval(timerId.current) }
}, []);
async function dealCard() {
if (remaining.current === 0) {
alert('You are out of cards');
isDealing.current = false;
button.current.innerText = "Shuffle deck";
button.current.onClick = shuffleDeck;
return;
}
console.log(button);
const res = await axios.get(`http://deckofcardsapi.com/api/deck/${deckId.current}/draw/?count=1`);
remaining.current = res.data.remaining;
setSrc(res.data.cards[0].image);
};
async function shuffleDeck() {
const res = await axios.get(`http://deckofcardsapi.com/api/deck/${deckId.current}/shuffle/`);
setSrc('');
button.current.innerText = "Start dealing cards";
button.current.onClick = toggleDealing;
}
function toggleDealing() {
isDealing.current = !isDealing.current;
button.current.innerText = "Start dealing cards";
};
return (
<>
<button onClick={toggleDealing} ref={button}>{isDealing.current ? "Stop dealing cards" : "Start dealing cards"}</button>
<div>
<img className="CardBox" src={src} alt="a card" />
</div>
</>
)
};
export default CardTable2;
I've tried setting the button.current.onClick to the function, as seen above, but it doesn't actually seem to have an effect. Am I missing something?
I think what's happening is the return is just putting back {toggleDealing} every time.
Why not get rid of the ref and just have a generic function handle both cases?
const [buttonMode, setButtonMode] = useState("shuffle")
function handleClick() {
if (buttonMode === "deal") {
deal()
} else if (buttonMode === "shuffle") {
shuffle()
}
}

The component isn't updating when I pass in a filtered variable on a timer

So I was trying to implement a filter that is controlled by a search bar input. So I think part of the problem is that I have this filter hooked on a timer so that while the user is typing into the search bar, it isn't re-running for each letter typed in.
What it is currently doing is that after the item is typed in the search bar, the timer goes off and the filters are working but it doesn't appear that the app is re-rendering with the new filtered variable.
I suspect that it might have something to do with useEffect but I'm having trouble wrapping my head around it and it wasn't working out for whatever I was doing with it.
Here's the code:
const RecipeCards = (props) => {
const inputTypingRef = useRef(null);
let preparingElement = props.localRecipes;
let cardElement;
let elementsSorted;
const ingredientCountSort = (recipes) => {
elementsSorted = ...
}
const elementRender = (element) => {
cardElement = element.map((rec) => (
<RecipeCard
name={rec.name}
key={rec.id}
ingredients={rec.ingredients}
tags={rec.tags}
removeRecipe={() => props.onRemoveIngredients(rec.id)}
checkAvail={props.localIngredients}
/>
));
ingredientCountSort(cardElement);
};
if (inputTypingRef.current !== null) {
clearTimeout(inputTypingRef.current);
}
if (props.searchInput) {
inputTypingRef.current = setTimeout(() => {
inputTypingRef.current = null;
if (props.searchOption !== "all") {
preparingElement = props.localRecipes.filter((rec) => {
return rec[props.searchOption].includes(props.searchInput);
});
} else {
preparingElement = props.localRecipes.filter((rec) => {
return rec.includes(props.searchInput);
});
}
}, 600);
}
elementRender(preparingElement);
return (
<div className={classes.RecipeCards}>{!elementsSorted ? <BeginPrompt /> : elementsSorted}</div>
);
};
Don't worry about ingredientCountSort() function. It's a working function that just rearranges the array of JSX code.
Following up to my comment in original question. elementsSorted is changed, but it doesn't trigger a re-render because there isn't a state update.
instead of
let elementsSorted
and
elementsSorted = ...
try useState
import React, { useState } from 'react'
const RecipeCards = (props) => {
....
const [ elementsSorted, setElementsSorted ] = useState();
const ingredientCountSort = () => {
...
setElementsSorted(...whatever values elementsSorted supposed to be here)
}
Reference: https://reactjs.org/docs/hooks-state.html
I used useEffect() and an additional useRef() while restructuring the order of functions
const RecipeCards = (props) => {
//const inputTypingRef = useRef(null);
let preparingElement = props.localRecipes;
let finalElement;
const [enteredFilter, setEnteredFilter] = useState(props.searchInput);
let elementsSorted;
const [elementsFiltered, setElementsFiltered] = useState();
const refTimer = useRef();
const filterActive = useRef(false);
let cardElement;
useEffect(() => {
setEnteredFilter(props.searchInput);
console.log("updating filter");
}, [props.searchInput]);
const filterRecipes = (recipes) => {
if (enteredFilter && !filterActive.current) {
console.log("begin filtering");
if (refTimer.current !== null) {
clearTimeout(refTimer.current);
}
refTimer.current = setTimeout(() => {
refTimer.current = null;
if (props.searchOption !== "all") {
setElementsFiltered(recipes.filter((rec) => {
return rec.props[props.searchOption].includes(enteredFilter);
}))
} else {
setElementsFiltered(recipes.filter((rec) => {
return rec.props.includes(enteredFilter);
}))
}
filterActive.current = true;
console.log(elementsFiltered);
}, 600);
}else if(!enteredFilter && filterActive.current){
filterActive.current = false;
setElementsFiltered();
}
finalElement = elementsFiltered ? elementsFiltered : recipes;
};
const ingredientCountSort = (recipes) => {
console.log("sorting elements");
elementsSorted = recipes.sort((a, b) => {
...
filterRecipes(elementsSorted);
};
const elementRender = (element) => {
console.log("building JSX");
cardElement = element.map((rec) => (
<RecipeCard
name={rec.name}
key={rec.id}
ingredients={rec.ingredients}
tags={rec.tags}
removeRecipe={() => props.onRemoveIngredients(rec.id)}
checkAvail={props.localIngredients}
/>
));
ingredientCountSort(cardElement);
};
//begin render /////////////////// /// /// /// /// ///
elementRender(preparingElement);
console.log(finalElement);
return (
<div className={classes.RecipeCards}>{!finalElement[0] ? <BeginPrompt /> : finalElement}</div>
);
};
There might be redundant un-optimized code I want to remove on a brush-over in the future, but it works without continuous re-renders.

Make a React component rerender when data class property change

In my Typescript app, there's a class that represents some data. This class is being shared end to end (both front-and-back ends use it to structure the data). It has a property named items which is an array of numbers.
class Data {
constructor() {
this.items = [0];
}
addItem() {
this.items = [...this.items, this.items.length];
}
}
I'm trying to render those numbers in my component, but since modifying the class instance won't cause a re-render, I have to "force rerender" to make the new items values render:
const INSTANCE = new Data();
function ItemsDisplay() {
const forceUpdate = useUpdate(); // from react-use
useEffect(() => {
const interval = setInterval(() => {
INSTANCE.addItem();
forceUpdate(); // make it work
}, 2000);
return () => clearInterval(interval);
}, []);
return (
<div>
<h1>with class:</h1>
<div>{INSTANCE.items.map(item => <span>{item}</span>)}</div>
</div>
);
}
While this works, it has one major drawback: addItem() is not the only modification done to INSTANCE; This class has actually around 10 to 15 properties that represent different data parts. So, doing forceUpdate() wherever a modification happens is a nightmare. Not no mention, if this instance will be modified outside the component, I won't be able to forceUpdate() to sync the change with the component.
Using useState([]) to represent items will solve this issue, but as I said Data has a lot of properties, so as some functions. That's another nightmare.
I would like to know what's the best way of rendering data from a class instance, without rerender hacks or unpacking the whole instance into a local component state.
Thanks!
Here's a Codesandbox demo that shows the differences between using a class and a local state.
Here is an example of how you can make Data instance observable and use Effect in your components to observe changes in Data instance items:
const { useState, useEffect } = React;
class Data {
constructor() {
this.data = {
users: [],
products: [],
};
this.listeners = [];
}
addItem(type, newItem) {
this.data[type] = [...this.data[type], newItem];
//notify all listeners that something has been changed
this.notify();
}
addUser(user) {
this.addItem('users', user);
}
addProduct(product) {
this.addItem('products', product);
}
reset = () => {
this.data.users = [];
this.data.products = [];
this.notify();
};
notify() {
this.listeners.forEach((l) => l(this.data));
}
addListener = (fn) => {
this.listeners.push(fn);
//return the remove listener function
return () =>
(this.listeners = this.listeners.filter(
(l) => l !== fn
));
};
}
const instance = new Data();
let counter = 0;
setInterval(() => {
if (counter < 10) {
if (counter % 2) {
instance.addUser({ userName: counter });
} else {
instance.addProduct({ productId: counter });
}
counter++;
}
}, 500);
//custom hook to use instance
const useInstance = (instance, fn = (id) => id) => {
const [items, setItems] = useState(fn(instance.data));
useEffect(
() =>
instance.addListener((items) => setItems(fn(items))),
[instance, fn]
);
return items;
};
const getUsers = (data) => data.users;
const getProducts = (data) => data.products;
const Users = () => {
const users = useInstance(instance, getUsers);
return <pre>{JSON.stringify(users)}</pre>;
};
const Products = () => {
const products = useInstance(instance, getProducts);
return <pre>{JSON.stringify(products)}</pre>;
};
const App = () => {
const reset = () => {
instance.reset();
counter = 0;
};
return (
<div>
<button onClick={reset}>Reset</button>
<div>
users:
<Users />
</div>
<div>
products:
<Products />
</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Categories