react show job progress - reactjs

I'm making requests inside a loop and would like to show its progress on a div:
import React, {useState, useEffect} from 'react';
import axios from 'axios';
function Job() {
const [part, setPart] = useState(0);
const [message, setMessage] = useState(null);
async function executeJob() {
const N_PARTS = 10;
for (let i = 0; i < N_PARTS; i++) {
setPart(i);
setMessage(`Current part: ${part}`);
await axios.post(`/parts/${i}/doit`);
}
}
return (
<>
<div>{message}</div>
<button onClick={executeJob}>Execute</button>
</>
);
}
When I run this, the requests were made correctly, but it doesn't display message correctly. I suspect that I can't set states sequentially this way and have to put it inside an effect. I'm new to using React hooks, please let me know the correct way.

I've made a simple example.
export default function App() {
const [ part, setPart] = React.useState(0);
async function execute(){
for await(let i of new Array(10).fill(0)){
setPart((prev)=>prev+1);
console.log(i);
await fetch(`https://randomuser.me/api`).then((data)=>{
console.log(data);
})
}
}
return (
<div className="App">
<h1>Hello CodeSandbox {part}</h1>
<button onClick={execute}>Excute</button>
</div>
);
}
Here is the same example in codesandbox
https://codesandbox.io/s/adoring-cannon-srppr
you can use for-loop with async-await.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of

Related

React State and Arrays - Double rendering causes elements duplication

I am developing a fullstack blockchain Nft Dapp with React, Ethers and Solidity. I have made some routes and a mint page with wallet connection and mintbutton. Under the mint section there's the personal collection, where infos about property and metadata are retrieved from contract.
That's the collection component code.
import { useEffect, useState } from "react";
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Dino from "./Dino";
import { Contract, providers } from "ethers";
import { truncateAddress } from "./utils";
import { useWeb3React } from "#web3-react/core";
import { abi } from './abi';
export default function MyDinos() {
const { library, account} = useWeb3React();
const [dinosUri, setDinosUri] = useState([]);
const dinosTD = dinosUri.map((dino) => {
return (
<Dino key={dino} uriMetadata={dino} />
)
});
useEffect(() => {
if (!account) return;
if (!library) return;
const getDinosUri = async () => {
try {
const provider = await library.provider;
const web3Provider = new providers.Web3Provider(provider);
const signer = web3Provider.getSigner();
const contract = new Contract(process.env.REACT_APP_CONTRACT_ADDRESS, abi, signer);
const idArray = await contract.tokensOfWallet(account);
const idArrayFormatted = idArray.map(id => id.toNumber()).sort();
const uri = await contract.tokenURI(1);
const uriInPieces = uri.split("/");
const tmpDinos = [];
idArrayFormatted.forEach(id => {
const uriFormatted = `https://ipfs.io/ipfs/${uriInPieces[2]}/${id}`;
tmpDinos.push(uriFormatted);
//setDinosUri(prevArray => [...prevArray, uriFormatted])
});
setDinosUri(tmpDinos);
} catch (err) {
console.log(err);
}
}
getDinosUri();
return () => {
setDinosUri([]);
}
}, [library, account]);
return (
<>
{dinosUri.length > 0 &&
<div className='late-wow appear'>
<div className='svg-border-container bottom-border-light'></div>
<Container fluid className='sfondo-light py-4'>
<Container className='wow-container'>
<h2 className='wow appear mb-4 text-center'>Account: {truncateAddress(account)}</h2>
<h3 className='wow appear mb-4 text-center'>Dinos owned: {dinosUri.length} Dinos</h3>
<h4 className='wow appear mb-4 text-center'>Races won: COMING SOON</h4>
</Container>
</Container>
<div className='svg-border-container'></div>
<Container fluid className='sfondo-dark py-4'>
<Container>
<h2 className='mb-4'>My {dinosUri.length} Dinos</h2>
<Row className='my-5'>
{[...dinosTD]}
</Row>
</Container>
</Container>
</div>
}
</>
)
}
I managed to get the wanted result using a temporary variable tmpDinos to store the array of info, because if I used the commented method below //setDinosUri(prevArray => [...prevArray, uriFormatted]) on the first render I get the correct list, but if I change route and then get back to mint page, the collection is doubled. With the temp variable I cheated on the issue because it saves 2 times the same array content and it works good, but I don't think that's the correct React way to handle this issue. How can I get the previous code working? May it be a useEffect dependancy thing?
Thanks in advance for your attention.
A simple solution is to check if dinosUri is populated before setting its value.
if (dinosUri.length === 0) setDinosUri(prevArray => [...prevArray, uriFormatted])

input value not updating when mutating state

While creating a little project for learning purposes I have come across an issue with the updating of the input value. This is the component (I have tried to reduce it to a minimum).
function TipSelector({selections, onTipChanged}: {selections: TipSelectorItem[], onTipChanged?:(tipPercent:number)=>void}) {
const [controls, setControls] = useState<any>([]);
const [tip, setTip] = useState<string>("0");
function customTipChanged(percent: string) {
setTip(percent);
}
//Build controls
function buildControls()
{
let controlList: any[] = [];
controlList.push(<input className={styles.input} value={tip.toString()} onChange={(event)=> {customTipChanged(event.target.value)}}></input>);
setControls(controlList);
}
useEffect(()=>{
console.log("TipSelector: useEffect");
buildControls();
return ()=> {
console.log("unmounts");
}
},[])
console.log("TipSelector: Render -> "+tip);
return (
<div className={styles.tipSelector}>
<span className={globalStyles.label}>Select Tip %</span>
<div className={styles.btnContainer}>
{
controls
}
</div>
</div>
);
}
If I move the creation of the input directly into the return() statement the value is updated properly.
I'd move your inputs out of that component, and let them manage their own state out of the TipSelector.
See:
https://codesandbox.io/s/naughty-http-d38w9
e.g.:
import { useState, useEffect } from "react";
import CustomInput from "./Input";
function TipSelector({ selections, onTipChanged }) {
const [controls, setControls] = useState([]);
//Build controls
function buildControls() {
let controlList = [];
controlList.push(<CustomInput />);
controlList.push(<CustomInput />);
setControls(controlList);
}
useEffect(() => {
buildControls();
return () => {
console.log("unmounts");
};
}, []);
return (
<div>
<span>Select Tip %</span>
<div>{controls}</div>
</div>
);
}
export default TipSelector;
import { useState, useEffect } from "react";
function CustomInput() {
const [tip, setTip] = useState("0");
function customTipChanged(percent) {
setTip(percent);
}
return (
<input
value={tip.toString()}
onChange={(event) => {
customTipChanged(event.target.value);
}}
></input>
);
}
export default CustomInput;
You are only calling buildControls once, where the <input ... gets its value only that single time.
Whenever React re-renders your component (because e.g. some state changes), your {controls} will tell React to render that original <input ... with the old value.
I'm not sure why you are storing your controls in a state variable? There's no need for that, and as you noticed, it complicates things a lot. You would basically require a renderControls() function too that you would replace {controls} with.

Updating array using react hooks

I am making an application using the Upsplash API.
Upon rendering I want to display 30 images, witch works correctly.
import React, { useState, useEffect } from "react"
const ContextProvider =({ children }) =>{
const [allPhotos, setAllPhotos] = useState([])
const [cartItems, setCartItems] = useState([])
const [imageQuery, setImageQuery] = useState('')
useEffect(() => {
const url = `https://api.unsplash.com/photos?page=5&per_page=30&client_id=${process.env.REACT_APP_UNSPLASH_KEY}`
async function getPhotos() {
const photosPromise = await fetch(url)
const photos = await photosPromise.json()
setAllPhotos(photos)
}
getPhotos()
},[])
I then pass AllPhotos to my Photos.js using my context, and map over allPhotos, passing the photo to my Image component to display information about the image.
import React, {useContext} from "react"
import {Context} from "../Context"
function Photos(){
const {allPhotos} = useContext(Context)
const imageElements = allPhotos.map((photo,index) =>(
<Image key={photo.id} photo={photo}/>
))
return(
<>
<main>
{imageElements}
</main>
</>
)
}
export default Photos
const Image = ({ photo }) => {
return (
<div
<img src={photo.urls.thumb} className="image-grid" alt="" />
</div>
)
}
From here the images from the API display and everything is working correctly.
What I want to do now is add a search query, where the users can search for certain images.
I made a component for the input value
import React, { useContext } from "react"
import {Context} from "../../Context"
const QueryInput = () =>{
const {imageQuery, setImageQuery, SearchImage} = useContext(Context)
return(
<form onSubmit={SearchImage} >
<label>
Search Photos
<input
type="text"
className="query-input"
placeholder="Search Images"
value={imageQuery}
onChange={(e) => setImageQuery(e.target.value) }
/>
</label>
<button type="submit">Search Image</button>
</form>
)
}
export default QueryInput
I made a searchQuery function in my context
const SearchImage = async (e) =>{
e.preventDefault()
const queryUrl = `https://api.unsplash.com/search/photos?
age=5&per_page=30&query=${imageQuery}&client_id=${APP_KEY}`
const response = await fetch(queryUrl)
const queryPhotos = await response.json();
setAllPhotos(prevState => [...prevState, ...queryPhotos])
}
Everything works so far, I can console.log(queryPhotos) and get the users images of the query they searched for. If I search for "stars" I will get a bunch of images with stars.
What im having trouble doing is mapping through allPhotos again and displaying the query search images.
The error im having is
TypeError: queryPhotos is not iterable
I have been at this for awhile. Any information/advice would be greatly appreciated. Any questions about the code or need additional information I can provide it. THANK YOU.
In short.
queryPhotos is not an array.
unsplash api response for api /photos and /search/photos is a bit different. One return an array, while the other is an object, you need to access photos in results
So, change this line from
setAllPhotos(prevState => [...prevState, ...queryPhotos])
to
setAllPhotos(prevState => [...prevState, ...queryPhotos.results])
Should fix your problem.

What's wrong with my custom hook in React?

Hi Stack Overflow Community!
I am learning react and now I am practicing custom hooks. I get the example data from here:
https://jsonplaceholder.typicode.com/posts
I've got two components.
App.js
import React from "react";
import useComments from "./hooks/useComments"
const App = () => {
const Comments = useComments();
const renderedItems = Comments.map((comment) => {
return <li key={comment.id}>{comment.title}</li>;
});
return (
<div>
<h1>Comments</h1>
<ul>{renderedItems}</ul>
</div>
);
}
export default App;
useComments.js
import {useState, useEffect} from "react";
const useComments = () => {
const [Comments, setComments] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts", {
"mode": "cors",
"credentials": "omit"
}).then(res => res.json()).then(data => setComments(data))
}, []);
return [Comments];
};
export default useComments;
My output looks like this and i don't know why. There are no warnings or errors.
This line makes Comments be an array:
const [Comments, setComments] = useState([]);
...and then you're wrapping it in an additional array:
return [Comments];
But when you use it, you're treating it as a single dimensional array.
const Comments = useComments();
const renderedItems = Comments.map...
So you'll just need to line those two up. If you want two levels of array-ness (perhaps because you plan to add more to your hook, so that it's returning more things than just Comments), then the component will need to remove one of them. This can be done with destructuring, as in:
const [Comments] = useComments();
Alternatively, if you don't need that complexity, you can change your hook to not add the extra array, and return this:
return Comments;

Setting state without re-rendering with useEffect not working

I simply need my state value to change based on screen sizing live time. Even though my screen size changes, my count value stays the same unless I reload the page. this needs to update live for better responsive design. Here is my code. Thanks!
import React, { useState, useEffect } from 'react';
import Carousel from '#brainhubeu/react-carousel';
import '#brainhubeu/react-carousel/lib/style.css';
import { useMediaQuery } from 'react-responsive'
const MyCarousel = () => {
useEffect(() => {
loadMediaQuery();
}, []);
const [count, setCount] = useState(0);
const loadMediaQuery = () =>{
if (tablet)
setCount(1)
if (phone)
setCount(2)
if (desktop)
setCount(3)
}
const tablet = useMediaQuery({
query: '(max-width: 876px)'
})
const phone = useMediaQuery({
query: '(max-width: 576px)'
})
const desktop = useMediaQuery({
query: '(min-width: 876px)'
})
return (
<div>
<Carousel slidesPerPage={count} >
<img className="image-one"/>
<img className="image-two"/>
<img className="image-three"/>
</Carousel>
</div>
);
}
That's because your useEffect has no dependencies so it loads one time only after the component has been mounted.
To fix that you should have the following code:
import React, { useState, useEffect } from 'react';
import Carousel from '#brainhubeu/react-carousel';
import '#brainhubeu/react-carousel/lib/style.css';
import { useMediaQuery } from 'react-responsive';
const MyCarousel = () => {
const tablet = useMediaQuery({
query: '(max-width: 876px)'
});
const phone = useMediaQuery({
query: '(max-width: 576px)'
});
const desktop = useMediaQuery({
query: '(min-width: 876px)'
});
useEffect(() => {
loadMediaQuery();
}, [tablet, phone, desktop]);
const [count, setCount] = useState(0);
const loadMediaQuery = () =>{
if (tablet)
setCount(1)
else if (phone)
setCount(2)
else if (desktop)
setCount(3)
}
return (
<div>
<Carousel slidesPerPage={count} >
<img className="image-one"/>
<img className="image-two"/>
<img className="image-three"/>
</Carousel>
</div>
);
}
useEffect means: runs the callback function that's passed to useEffect after this component is rendered, and since you passed an empty array as the 2nd argument, it means useEffect is executed once, only the first time when this component gets rendered (and not when the state changes) , since your function call is inside useEffect, it will work only when you reload the page, you can either add the variables to the empty array like Mohamed Magdy did, or simply call loadMediaQuery again outside useEffect
Expanding on the custom hook provided in this answer, you can do something like
const [width, height] = useWindowSize();
useEffect(() => {
loadMediaQuery();
}, [width]);
That useEffect function says: Execute loadMediaQuery() every time the value of width changes.

Resources