How can you instantiate a .gif file in a React app? - javascript

I have a React app that does 3 things; displays the elements in an array, adds an element on button press and removes an element on button press.
Inside of my image, I am displaying my local .gif file. My .gif file is a single animation .gif, I turned off infinite looping because I want the .gif to spawn in and then remain static.
My error comes when I add a second .gif element to my array. Only the first element displays its animation and the rest display the final slide in the .gif.
I believe that I may be able to solve my issue if I manage to instantiate the element but I am not sure how I would go about doing that.
Here is an excerpt from my code::
function App(){
const [numbValue, numbUpdate] = useState(0);
const [starsValue, starsUpdate] = useState(null);
function DisplayGIF(numbToDisp){
let value=[];
for(let i=0;i<numbToDisp;i++){
value.push(<img className="icon" src={process.env.PUBLIC_URL + "/once-star-filled.gif"} alt="animated star"/>)
}
starsUpdate(<>{value.map(element=>{return element;})}</>);
}
function Add(){
numbUpdate(numbValue+1);
DisplayGIF(numbValue+1);
}
function Sub(){
numbUpdate(numbValue-1);
DisplayGIF(numbValue-1);
}
return(<>
<p onClick={Add}>+</p>
{starsValue}
<p onClick={Sub}>-</p>
</>);
}
Output::
First add :: displays 1 image that is animated until the end
Consecutive adds :: displays x images that display the final frame in the animation

Please, try this one.
function App() {
const [stars, setStars] = useState(0);
return (
<React.Fragment>
<p onClick={() => setStars((s) => s + 1)}>+</p>
{new Array(stars).fill().map((s, ind) => {
return <Star key={ind}></Star>;
})}
<p onClick={() => setStars((s) => (s === 0 ? 0 : s - 1))}>-</p>
</React.Fragment>
);
}
export function Star(props) {
const [id] = useState(Math.random()); // Generate unique id for this item
return (
<img
className="icon"
src={`${process.env.PUBLIC_URL}/once-star-filled.gif?id=${id}`}
alt="animated star"
/>
);
}
I fix some notation errors, like function name should be started from small letter.
Moreover, you could use only one state to store and render stars gif.
Also, it is better to create another React function element <Star />. In this way you can reuse this gif later, and add some props, for instance different alter text, src attribute and et al.
[Update 1]
I disagree with #Karl comment, there is a significant flaw in his solution: when the src is formed using the ind, elements will be animated only once (ex: remove element with ind=2 and add again element with ind=2 give us a static image).
So I decided to supplement my answer with one more option, which I would not use in production, but it is interesting and solves the OP's problem.
What is the new solution? We fetch the image through fetch, convert it to dataUrl, delete the first part represented metadata, and pass it to the Star elements for rendering as src props.
Each element adds meta-information with its own Id, which does not affect the file itself, but the browser perceives the picture as new. Therefore, all start animates every time their appear on page.
import React, { useState, useRef } from "react";
import "./App.css";
function App() {
const [stars, setStars] = useState(0);
const [data, setData] = useState(null);
const ref = useRef(null);
React.useEffect(() => {
fetch("./ezgif-2-110436c6c12b.gif")
.then((res) => res.blob())
.then(async (text) => {
const reader = new FileReader();
reader.onload = (ev) => {
setData(
ev.currentTarget.result.replace("data:image/gif;base64", "")
);
};
reader.readAsDataURL(text);
});
}, []);
return (
<React.Fragment>
<p onClick={() => setStars((s) => s + 1)}>+</p>
{data &&
new Array(stars).fill().map((s, ind) => {
return <Star src={data} key={ind}></Star>;
})}
<p onClick={() => setStars((s) => (s === 0 ? 0 : s - 1))}>-</p>
</React.Fragment>
);
}
/**
*
* #param {{src: string}} props
* #returns
*/
export function Star(props) {
const [id] = useState(Math.random());
return (
<img
className="icon"
src={`data:image/gif;base64;${id}` + props.src}
alt="animated star"
/>
);
}
export default App;

Related

React state updating in the dom but not in function

I have an onClick event on an input field called handleLinkChange that validates its content (a YouTube link) and if it's valid calls a function that extract its YouTube ids.
Within the onClick event I update the links state so the user sees if the link is valid and the extraction of ids is in progress (indicated by a loading wheel in the input field). At this point the state updates as desired in the DOM. However, when I arrive in the getIdsFromLink function after setting the state, the links state is still at the initial value. This makes it impossible to alter the links, for example to replace the loading indicator of the link with a checkmark when the ids have been parsed.
// Links state
const [links, setLinks] = useState([{ link: '', state: IS_VALID, errorMessage: '' }])
// onChange event handler
const handleLinkChange = (event, index) => {
const clonedLinks = [...links]
let value = decodeURI(event.target.value.trim())
const { passesRegex, validityMessage } = checkLinkValidity(value)
clonedLinks[index] = {
link: value,
state: passesRegex ? IS_VALIDATING_SERVER_SIDE : IS_ERROR,
errorMessage: validityMessage,
}
setLinks(clonedLinks)
if (clonedLinks[index] !== '' && passesRegex) {
getIdsFromLink(clonedLinks[index].link, index)
}
}
// Parser
const getIdsFromLink = (link, index) => {
console.log('links state', links) // initial state
socket.emit('getIds', link)
socket.on('idsParsed', newIds => {
console.log('links state after parse', links) // initial state
})
}
// Shortened return
return (
links.map((link, index) => (
<Input
value={link.link}
onChange={event => handleLinkChange(event, index)}
/>
{link.link && (
<FontAwesomeIcon
icon={link.state === IS_VALID ? faCheck : link.state === IS_ERROR ? faTimes : faSpinner}
/>
)}
)
))
I know that states are asynchronous and I also tried watching for state changes with useEffect, but I'm unable to refactor my code in that way and I have another state object that heavily depends on the links state, so I gave up on that one.
Even when I try to use flushSync to update the state synchronously, I have the same effect.
I very much appreciate your help! Thanks!
I'm gonna answer my question as I figured it out thanks to Nick in the comments.
Basically I kept the handleLinkChange function unchanged. But in the getIdsFromLink function, when the link has been parsed from the server - that's where I have to update the state - I used setLinks with a function as parameter. The function receives an argument which resembles the current state. All I had to do is to make a copy of the object (for the rerender to work), then make my changes to it and finally return the new object.
const getIdsFromLink = (link, index) => {
socket.emit('getIds', link)
socket.once('idsParsed', ids => {
// Function parameter to get current state
setLinks(_currentLinks => {
const currentLinks = [..._currentLinks] // copy to force rerender
currentLinks[index].state = IS_VALID
currentLinks[index].ids = ids
console.log('finished currentLinks', currentLinks) // newest state
return currentLinks
})
})
}

React, component not re-rendering after change in an array state (not the same as others)

I'm trying to make a page that gets picture from a server and once all pictures are downloaded display them, but for some reason the page doesn't re-render when I update the state.
I've seen the other answers to this question that you have to pass a fresh array to the setImages function and not an updated version of the previous array, I'm doing that but it still doesn't work.
(the interesting thing is that if I put a console.log in an useEffect it does log the text when the array is re-rendered, but the page does not show the updated information)
If anyone can help out would be greatly appreciated!
Here is my code.
export function Profile() {
const user = JSON.parse(window.localStorage.getItem("user"));
const [imgs, setImages] = useState([]);
const [num, setNum] = useState(0);
const [finish, setFinish] = useState(false);
const getImages = async () => {
if (finish) return;
let imgarr = [];
let temp = num;
let filename = "";
let local = false;
while(temp < num+30) {
fetch("/get-my-images?id=" + user.id + "&logged=" + user.loggonToken + "&num=" + temp)
.then(response => {
if(response.status !== 200) {
setFinish(true);
temp = num+30;
local = true;
}
filename = response.headers.get("File-Name");
return response.blob()
})
.then(function(imageBlob) {
if(local) return;
const imageObjectURL = URL.createObjectURL(imageBlob);
imgarr[temp - num] = <img name={filename} alt="shot" className="img" src={imageObjectURL} key={temp} />
temp++;
});
}
setNum(temp)
setImages(prev => [...prev, ...imgarr]);
}
async function handleClick() {
await getImages();
}
return (
<div>
<div className="img-container">
{imgs.map(i => {
return (
i.props.name && <div className="img-card">
<div className="img-tag-container" onClick={(e) => handleView(i.props.name)}>{i}</div>
<div className="img-info">
<h3 className="title" onClick={() => handleView(i.props.name)}>{i.props.name.substr(i.props.name.lastIndexOf("\\")+1)}<span>{i.props.isFlagged ? "Flagged" : ""}</span></h3>
</div>
</div>
)
})}
</div>
<div className="btn-container"><button className="load-btn" disabled={finish} onClick={handleClick}>{imgs.length === 0 ? "Load Images" : "Load More"}</button></div>
</div>
)
}
I think your method of creating the new array is correct. You are passing an updater callback to the useState() updater function which returns a concatenation of the previous images and the new images, which should return a fresh array.
When using collection-based state variables, I highly recommend setting the key property of rendered children. Have you tried assigning a unique key to <div className="img-card">?. It appears that i.props.name is unique enough to work as a key.
Keys are how React associates individual items in a collection to their corresponding rendered DOM elements. They are especially important if you modify that collection. Whenever there's an issue with rendering collections, I always make sure the keys are valid and unique. Even if adding a key doesn't fix your issue, I would still highly recommend keeping it for performance reasons.
It is related to Array characteristics of javascript.
And the reason of the console log is related with console log print moment.
So it should be shown later updated for you.
There are several approaches.
const getImages = async () => {
... ...
setNum(temp)
const newImage = [...prev, ...imgarr];
setImages(prev => newImage);
}
const getImages = async () => {
... ...
setNum(temp)
setImages(prev => JOSN.parse(JSON.object([...prev, ...imgarr]);
}
const getImages = async () => {
... ...
setNum(temp)
setImages(prev => [...prev, ...imgarr].slice(0));
}
Maybe it could work.
Hope it will be helpful for you.
Ok the problem for me was the server was not sending a proper filename header so it was always null so the condition i.props.name was never true... lol sorry for the confusion.
So the moral of this story is, always make sure that it's not something else in your code that causes the bad behavior before starting to look for other solutions...

data updating before waiting for api call react

I am working on a react app where I have table with scroller and on every scroll I am updating the page number and making a subsquent api call with the updated page number but the page number is updating so fast that it exceeds the limit of page number and the api returns empty array and that leads to imcomplete data.
Here's my code:
handleScroll=async ({ scrollTop }) => {
console.log('hey');
if (this.props.masterName && this.props.codeSystem) {
const params = {};
await this.props.setPageNumber(this.props.page_num + 1);
params.code_system_category_id = this.props.masterName;
params.code_systems_id = this.props.codeSystem;
params.page_num = this.props.page_num;
if (this.props.entityName) params.entity_name = this.props.entityName;
if (this.props.status) params.status = this.props.status;
console.log(params);
await this.props.fetchCodeSets(params);
}
}
This is the function that will get called on every scroll,on every scroll I am incrementing the page number by 1 using await and also making a api call as this.props.fetchCodeSets using await so that scroll doesnt exceed before completing the api call,but the scroll keeps getting called and it leads to the above explained error.
Here's my table with scroll:
<StyledTable
height={250}
width={this.props.width}
headerHeight={headerHeight}
rowHeight={rowHeight}
rowRenderer={this.rowRenderer}
rowCount={this.props.codeSets.length}
rowGetter={({ index }) => this.props.codeSets[index]}
LoadingRow={this.props.LoadingRow}
overscanRowCount={5}
tabIndex={-1}
className='ui very basic small single line striped table'
columnsList={columns}
onScroll={() => this.handleScroll('scroll')}
/>
I am using react-virtualized table and the docs can be found here:
https://github.com/bvaughn/react-virtualized/blob/master/docs/Table.md
Any leads can definitely help!
You are loading a new page on every scroll interaction. If the user scrolls down by 5 pixels, do you need to load an entire page of data? And then another page when the scrolls down another 2 pixels? No. You only need to load a page when you have reached the end of the available rows.
You could use some math to figure out which pages need to be loaded based on the scrollTop position in the onScroll callback and the rowHeight variable.
But react-virtualized contains an InfiniteLoader component that can handle this for you. It will call a loadMoreRows function with the startIndex and the stopIndex of the rows that you should load. It does not keep track of which rows have already been requested, so you'll probably want to do that yourself.
Here, I am storing the API responses in a dictionary keyed by index, essentially a sparse array, to support any edge cases where the responses come back out of order.
We can check if a row is loaded by seeing if there is data at that index.
We will load subsequent pages when the loadMoreRows function is called by the InfiniteList component.
import { useState } from "react";
import { InfiniteLoader, Table, Column } from "react-virtualized";
import axios from "axios";
const PER_PAGE = 10;
const ROW_HEIGHT = 30;
export default function App() {
const [postsByIndex, setPostsByIndex] = useState({});
const [totalPosts, setTotalPosts] = useState(10000);
const [lastRequestedPage, setLastRequestedPage] = useState(0);
const loadApiPage = async (pageNumber) => {
console.log("loading page", pageNumber);
const startIndex = (pageNumber - 1) * PER_PAGE;
const response = await axios.get(
// your API is probably like `/posts/page/${pageNumber}`
`https://jsonplaceholder.typicode.com/posts?_start=${startIndex}&_end=${
startIndex + PER_PAGE
}`
);
// This only needs to happen once
setTotalPosts(parseInt(response.headers["x-total-count"]));
// Save each post to the correct index
const posts = response.data;
const indexedPosts = Object.fromEntries(
posts.map((post, i) => [startIndex + i, post])
);
setPostsByIndex((prevPosts) => ({
...prevPosts,
...indexedPosts
}));
};
const loadMoreRows = async ({ startIndex, stopIndex }) => {
// Load pages up to the stopIndex's page. Don't load previously requested.
const stopPage = Math.floor(stopIndex / PER_PAGE) + 1;
const pagesToLoad = [];
for (let i = lastRequestedPage + 1; i <= stopPage; i++) {
pagesToLoad.push(i);
}
setLastRequestedPage(stopPage);
return Promise.all(pagesToLoad.map(loadApiPage));
};
return (
<InfiniteLoader
isRowLoaded={(index) => !!postsByIndex[index]}
loadMoreRows={loadMoreRows}
rowCount={totalPosts}
minimumBatchSize={PER_PAGE}
>
{({ onRowsRendered, registerChild }) => (
<Table
height={500}
width={300}
onRowsRendered={onRowsRendered}
ref={registerChild}
rowCount={totalPosts}
rowHeight={ROW_HEIGHT}
// return empty object if not yet loaded to avoid errors
rowGetter={({ index }) => postsByIndex[index] || {}}
>
<Column label="Title" dataKey="title" width={100} />
<Column label="Description" dataKey="body" width={200} />
</Table>
)}
</InfiniteLoader>
);
}
CodeSandbox Link
The placeholder API that I am using takes start and end indexes instead of page numbers, so going back and forth from index to page number to index in this example is silly. But I am assuming that your API uses numbered pages.

React: For Loop is not detecting Hook's State changes

Developing a React Aplication which primary functionality is to show each word of a sentence within a time interval previously set by the user (basically fast reading). I achieved this some days ago, but now im trying to implement a pause button (to pause the loop and get the actual word) and stuck.
I decided to add a Pause button, that calls a function that sets the pause state to true, and inside my loop, in the case this pause state is 'true', it breaks out of the loop. But the problem is, that this loop doesnt detect the change, I have a console.log in line 18, that is always logging false, although I had already clicked the pause button and changed the state to 'false' (And that is working because im also logging it in the useEffect and its loggin 'true')
So, my question here is, how can I solve this? Am I using react hooks wrong?
Code Sample:
const [time, setTime] = React.useState('500')
const [pause, setPause] = React.useState(false)
const go = async function(){
let textarray = text.split(' ') //I split my text
for(let i = 0; i < textarray.length; i++){
console.log(pause) //This keeps logging false although the change in pauseButton function
if(pause){
//Get the actual word => i
break;
}
else{
setValue(textarray[i]) //Show the actual word
await timeout(time) //Wait the time
}
}
}
const pauseButton = function(){
setPause(true)
}
function timeout(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
I think you might want to refactor this a bit. Consider setting up a useInterval() hook to run the counter. Then it will be easy to start and stop your counter.
Use useMemo hook for the the split textArray result, so it does not recalculate every refresh.
Then you can just get the value of textarray[i] anytime to place in your JSX.
This way you won't need any other async functions at all.
Basically, you don't understand javascript closures, so this is the reason why you're using hooks wrong in the context of the async function. You can't get state updates inside async function in a practical way. You should use useEffects or a custom hook (Live demo to play):
import React from "react";
import { useState } from "react";
import { useAsyncCallback } from "use-async-effect2";
import { CPromise } from "c-promise2";
function TestComponent(props) {
const [text, setText] = useState("one two three four five");
const [word, setWord] = useState("");
const go = useAsyncCallback(
function* (text, delay) {
const words = text.split(/\s+/);
for (const word of words) {
setWord(word);
yield CPromise.delay(delay);
}
},
{ states: true, cancelPrevios: true }
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo</div>
<input
value={text}
onChange={({ target }) => {
setText(target.value);
}}
/>
<div>{go.error ? go.error.toString() : word}</div>
{go.pending ? (
go.paused ? (
<button className="btn btn-warning" onClick={go.resume}>
Resume
</button>
) : (
<button className="btn btn-warning" onClick={go.pause}>
Pause
</button>
)
) : (
<button className="btn btn-warning" onClick={() => go(text, 1000)}>
Run
</button>
)}
{go.pending && (
<button className="btn btn-warning" onClick={go.cancel}>
Cancel request
</button>
)}
</div>
);
}

hook state doesn't update automaticlly

I wanted to create pagination in React. All data comes from store. In this code I wanted to implement search engine. On this time I don't have this but i wrote search method which simulate that. OK, it works but - after I click hello, it display only items from category 2 but it display all the time this same pages (in my case 3). If I click 2 times more, it display only 1 page. I added setCountItems and setPages into search becouse this hooks doesn't update automaticlly.
import React, { useEffect, useState, useRef } from 'react'
import { connect} from 'react-redux'
import Article from './article'
const ArticlesContainer = ({ articles }) => {
const [allItems, setAllItems] = useState(articles.list);
const [pageNumber, setPageNumber] = useState(1);
const [perSite, setPerSite] = useState(10);
const [totalItems, setCountItems] = useState(allItems.length);
const from = (pageNumber - 1) * perSite;
const to = ((pageNumber - 1) * perSite) + perSite;
const [pages, setPages] = useState(Math.ceil(totalItems / perSite));
const handlePageClick = (i) => {
setPageNumber(i);
}
const search = () => {
setAllItems(allItems.filter(x => x.category=== 2 ));
setCountItems(allItems.length);
setPages(Math.ceil(totalItems / perSite));
}
const Pagination = ({pages}) => {
let list = []
for(let i = 1; i<=pages; i++){
list.push(<li key={i} onClick={() => handlePageClick(i)}>{i}</li>)
}
return list;
}
return (
<React.Fragment>
<a onClick={search}>Hello</a>
{allItems.slice(from, to).map(article =>
<Article key={article.id} article={article} />
)}
<div className="row">
<ul>
<Pagination pages={pages} />
</ul>
</div>
</React.Fragment>
);
}
const mapStateToProps = state => ({
articles: state.articles
})
export default connect(mapStateToProps, null)(ArticlesContainer);
Where problem is?
You seem to be suffering from a common misunderstanding of how State works in React. Updating state, whether via this.setState in class or via the "update function" returned by the useState hook, doesn't "automagically" change the relevant state value then and there. In class components, that's because of how React's implementation of setState works (it's asynchronous), but with Hooks it should be perfectly obvious if you stop to think about it. setAllItems is a function, while allItems is an array - and they don't have anything directly to do each other. Calling setAllItems doesn't change the value of allItems - because how could it? allItems is just a variable, the only way to give it a new value is to directly mutate or reassign it - clearly calling a separate function, setAllItems, with an argument that isn't allItems, can't possibly do that.
What it instead does is schedule a rerender of the component - that is, schedules a subsequent call of your function that represents the component - and ensures that the useState call corresponding to allItems will then return value you set. But this is necessarily a rather indirect process. In particular, allItems will have the value you want on the next render of your component, but that search function won't be called (until the user clicks the button again), so the setCountItems(allItems.length); call won't automatically trigger with the "correct" length (the updated length after filtering).
In your case the solution to the problem is very simple. You've overcomplicated your component by introducing far too many state variables, most of which are dependent on each other. Instead of const [totalItems, setCountItems] = useState(allItems.length);, just put const totalItems = allItems.length; - then this will automatically be recalculated to the correct value on every render. You've no need of a setCountItems function, as you know that it will always be equal to allItems.length - it doesn't vary independently.
Similarly, you can vastly simplify much else in this component, since the only things which can vary independently, and therefore which needs to be part of state, are the article list and the page number. This is how I would rewrite your component:
const perSite = 10;
const ArticlesContainer = ({ articles }) => {
const [allItems, setAllItems] = useState(articles.list);
const [pageNumber, setPageNumber] = useState(1);
const totalItems = allItems.length;
const from = (pageNumber - 1) * perSite;
const to = ((pageNumber - 1) * perSite) + perSite;
const pages = Math.ceil(totalItems / perSite);
const handlePageClick = (i) => {
setPageNumber(i);
}
const search = () => {
setAllItems(allItems.filter(x => x.category=== 2 ));
}
const Pagination = ({pages}) => {
let list = []
for(let i = 1; i<=pages; i++){
list.push(<li key={i} onClick={() => handlePageClick(i)}>{i}</li>)
}
return list;
}
return (
<React.Fragment>
<a onClick={search}>Hello</a>
{allItems.slice(from, to).map(article =>
<Article key={article.id} article={article} />
)}
<div className="row">
<ul>
<Pagination pages={pages} />
</ul>
</div>
</React.Fragment>
);
}

Categories

Resources