React State and Arrays - Double rendering causes elements duplication - reactjs

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])

Related

Not understanding UseEffect rendering

In my useEffect, I sort an array based on a users input.
I created an extra useState const [tempFlights, setTempFlights] = React.useState([]); that I only use in my UseEffect. This is a .js file not a .jsx file (I am working on another persons code base).
In my useEffect I update setTempFlights and my date for flights updates, when I remove the update for setTempFlights, flights isn't update until the next time the user chooses an input.
Here is my code
import React, { useEffect, useState } from "react";
import "./index.style.css";
import FlightSearchItem from "./FlightSearchItem";
import FlightSearchFooter from "./FlightSearchFooter";
import useFetchFlightResults from "./useFetchFlightResults";
import SortBy from "./filters/SortBy";
import { SortByDefaultOption } from "./filters/SortBy/enums";
export default function FlightSearch() {
const [sortBy, setSortBy] = useState(SortByDefaultOption);
// Fetch Flights
const { flights } = useFetchFlightResults();
const [tempFlights, setTempFlights] = React.useState([]);
useEffect(() => {
let temp = [];
if (sortBy.value === "PRICE_LOW") {
temp = flights.sort((a, b) => a.price - b.price);
setTempFlights([...temp]);
} else if (sortBy.value === "BEST") {
temp = flights.sort((a, b) => a.score - b.score);
setTempFlights([...temp]);
} else {
temp = flights.sort((a, b) => {
return (
new Date(a.segmentsArray[0].arrivesAt) -
new Date(b.segmentsArray[0].arrivesAt)
);
});
setTempFlights([...temp]);
}
}, [sortBy]);
// Only show 10 flight results per page
let paginatedFlights = flights.slice(0, 8);
return (
<div className="row">
<div className="pane m-t-1">
<div className="pane-header search-results__header">
<div className="search-results__title">
<b>Select an outbound flight</b>
<p className="m-v-0 fade small">DEN → CHI</p>
</div>
<SortBy value={sortBy} onChange={setSortBy} />
</div>
{/* Display Flight Results */}
<div className="pane-content">
{Array.isArray(paginatedFlights) &&
paginatedFlights.map((flight) => (
<FlightSearchItem key={flight.id} flight={flight} />
))}
</div>
</div>
{/* Pagination */}
<FlightSearchFooter />
</div>
);
}
You can see that the page updates based off of flights not tempFlights.
What is going on here and how can I change it to not need setTempFlights([...temp]);
.sort() mutates the original array (flights in this case). This is why you are seeing updates even though you aren't using tempFlights. Oftentimes when using sort, you would create a copy of the original array beforehand to avoid mutating it.
const tempFlights = [...flights];
tempFlights.sort()
In the above, tempFlights ends up sorted and flights is left alone.
If I were rewriting your snippet, I wouldn't use an effect at all. The resulting array can easily be derived from the selected sort value and doesn't need to be held in state separately.
const [sortBy, setSortBy] = useState(SortByDefaultOption);
// Fetch Flights
const { flights } = useFetchFlightResults();
const getTempFlights = () => {
let temp = [...flights];
if (sortBy.value === "PRICE_LOW") {
temp.sort((a, b) => a.price - b.price);
} else if (sortBy.value === "BEST") {
temp.sort((a, b) => a.score - b.score);
} else {
temp.sort((a, b) => {
return (
new Date(a.segmentsArray[0].arrivesAt) -
new Date(b.segmentsArray[0].arrivesAt)
);
});
}
return temp;
}
const tempFlights = getTempFlights();
// Render logic as before
You could wrap getTempFlights in a useMemo hook if you're worried about recalculating the array each render, but it is often not consequential with typical data sets

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.

React Hook useEffect() run continuously although I pass the second params

I have problem with this code
If I pass the whole pagination object to the second parameters of useEffect() function, then fetchData() will call continuously. If I only pass pagination.current_page so It will call only one time, but when I set new pagination as you see in navigatePage() function, the useEffect() does not call to fetchData() although pagination has changed.
How to solve this. Thank you very much!
Besides I do not want the use useEffect() call when first time component mounted because the items is received from props (It is fetch by server, this is nextjs project).
import React, {useEffect, useState} from 'react';
import Filter from "../Filter/Filter";
import AdsListingItem from "../AdsListingItem/AdsListingItem";
import {Pagination} from "antd-mobile";
import styles from './AdsListing.module.css';
import axios from 'axios';
const locale = {
prevText: 'Trang trước',
nextText: 'Trang sau'
};
const AdsListing = ({items, meta}) => {
const [data, setData] = useState(items);
const [pagination, setPagination] = useState(meta);
const {last_page, current_page} = pagination;
const fetchData = async (params = {}) => {
axios.get('/ads', {...params})
.then(({data}) => {
setData(data.data);
setPagination(data.meta);
})
.catch(error => console.log(error))
};
useEffect( () => {
fetchData({page: pagination.current_page});
}, [pagination.current_page]);
const navigatePage = (pager) => {
const newPagination = pagination;
newPagination.current_page = pager;
setPagination(newPagination);
};
return (
<>
<Filter/>
<div className="row no-gutters">
<div className="col-md-8">
<div>
{data.map(item => (
<AdsListingItem key={item.id} item={item}/>
))}
</div>
<div className={styles.pagination__container}>
<Pagination onChange={navigatePage} total={last_page} current={current_page} locale={locale}/>
</div>
</div>
<div className="col-md-4" style={{padding: '15px'}}>
<img style={{width: '100%'}} src="https://tpc.googlesyndication.com/simgad/10559698493288182074"
alt="ads"/>
</div>
</div>
</>
)
};
export default AdsListing;
The issue is you aren't returning a new object reference. You save a reference to the last state object, mutate a property on it, and save it again.
const navigatePage = (pager) => {
const newPagination = pagination; // copy ref pointing to pagination
newPagination.current_page = pager; // mutate property on ref
setPagination(newPagination); // save ref still pointing to pagination
};
In this case the location in memory that is pagination remains static. You should instead copy all the pagination properties into a new object.
const navigatePage = (pager) => {
const newPagination = {...pagination}; // shallow copy into new object
newPagination.current_page = pager;
setPagination(newPagination); // save new object
};
To take it a step further you really should be doing functional updates in order to correctly queue up updates. This is in the case that setPagination is called multiple times during a single render cycle.
const navigatePage = (pager) => {
setPagination(prevPagination => {
const newPagination = {...prevPagination};
newPagination.current_page = pager;
});
};
In the case of pagination queueing updates may not be an issue (last current page set wins the next render battle), but if any state updates actually depend on a previous value then definitely use the functional update pattern,

Show loading state but also show previous results in React Concurrent gives a warning

UPDATE: Ok, it I misunderstood useDeferredValue, I thought it was more like a debounced value but it's not, you can define the timeout to be the time the old results will be shown.
So
const search = useDeferredValue(value, { timeoutMs: 10000 })
Gave me the desired effect, only it still show the warning right know.
Original
I want to have a search with the results below it, the search result should filter immediately based on the input of the text field. Then the query should be done debounced and the old results should show also when it takes less than e.g. 3000 m.s.
I'm working with the new concurrent mode in React and Relay experimental. I used the new useDeferredValue, documented on this page: https://reactjs.org/docs/concurrent-mode-reference.html#usetransition
But I got this warning:
Warning: Asynchronous triggered a user-blocking update that suspended.
The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes.
Refer to the documentation for useTransition to learn how to implement this pattern
I don't get this since it works but it still gives me a warning.
My code:
import React, {
Suspense,
useState,
// #ts-ignore - useDeferredValue does not exist yet in types
useDeferredValue,
// #ts-ignore - useDeferredValue does not exist yet in types
// useTransition,
useCallback,
ChangeEvent,
} from 'react'
import TextField from '#material-ui/core/TextField'
import LinearProgress from '#material-ui/core/LinearProgress'
import { graphql } from 'babel-plugin-relay/macro'
import { useLazyLoadQuery } from 'react-relay/hooks'
import {
FlowBlockFinderQuery,
FlowBlockFinderQueryResponse,
} from '../__generated__/FlowBlockFinderQuery.graphql'
import ErrorBoundaryWithRetry from '../helpers/ErrorBoundaryWithRetry'
interface RenderFuncProps {
search: string
filterSearch: string
}
function QueryResults({ search, filterSearch }: RenderFuncProps) {
const { blocks }: FlowBlockFinderQueryResponse = useLazyLoadQuery<
FlowBlockFinderQuery
>(
graphql`
query FlowBlockFinderQuery($search: String) {
blocks(search: $search) {
id
title
description
slug
blockType
}
}
`,
{ search },
{ fetchPolicy: 'store-or-network' }
)
return (
<div>
{blocks
.filter(
block =>
!filterSearch ||
block.title.toLowerCase().includes(filterSearch.toLowerCase())
)
.map(block => (
<div key={block.id} style={{ fontSize: 19 }}>
{block.title}
</div>
))}
</div>
)
}
function Results({ search, filterSearch }: RenderFuncProps) {
return (
<>
Zoekterm: {filterSearch}
<ErrorBoundaryWithRetry
fallback={({ error }) => <div>Er is iets foutgegaan</div>}
>
<Suspense fallback={<LinearProgress />}>
<QueryResults search={search} filterSearch={filterSearch} />
</Suspense>
</ErrorBoundaryWithRetry>
</>
)
}
export default function Asynchronous() {
const [value, setValue] = useState('')
// const [search, setSearch] = useState('')
const search = useDeferredValue(value, { timeoutMs: 3000 })
// const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG)
const onInputChange = useCallback(
(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
// startTransition(() => {
setValue(event.currentTarget.value)
// })
},
[setValue]
)
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<TextField
label="Nieuw of bestaand blok"
fullWidth
variant="outlined"
value={value}
onChange={onInputChange}
/>
<br />
<Results search={search} filterSearch={value} />
</div>
)
}
React docs "if some state update causes a component to suspend, that state update should be wrapped in a transition". You have to make the async request suspense compatible and fetch the query in useTransition.
Here is an example from react docs
function handleChange(e) {
const value = e.target.value;
// Outside the transition (urgent)
setQuery(value);
startTransition(() => {
// Inside the transition (may be delayed)
setResource(fetchTranslation(value));
});
}
And the link to code sandbox

UseEffect causes infinite loop with swipeable routes

I am using the https://www.npmjs.com/package/react-swipeable-routes library to set up some swipeable views in my React app.
I have a custom context that contains a dynamic list of views that need to be rendered as children of the swipeable router, and I have added two buttons for a 'next' and 'previous' view for desktop users.
Now I am stuck on how to get the next and previous item from the array of modules.
I thought to fix it with a custom context and custom hook, but when using that I am getting stuck in an infinite loop.
My custom hook:
import { useContext } from 'react';
import { RootContext } from '../context/root-context';
const useShow = () => {
const [state, setState] = useContext(RootContext);
const setModules = (modules) => {
setState((currentState) => ({
...currentState,
modules,
}));
};
const setActiveModule = (currentModule) => {
// here is the magic. we get the currentModule, so we know which module is visible on the screen
// with this info, we can determine what the previous and next modules are
const index = state.modules.findIndex((module) => module.id === currentModule.id);
// if we are on first item, then there is no previous
let previous = index - 1;
if (previous < 0) {
previous = 0;
}
// if we are on last item, then there is no next
let next = index + 1;
if (next > state.modules.length - 1) {
next = state.modules.length - 1;
}
// update the state. this will trigger every component listening to the previous and next values
setState((currentState) => ({
...currentState,
previous: state.modules[previous].id,
next: state.modules[next].id,
}));
};
return {
modules: state.modules,
setActiveModule,
setModules,
previous: state.previous,
next: state.next,
};
};
export default useShow;
My custom context:
import React, { useState } from 'react';
export const RootContext = React.createContext([{}, () => {}]);
export default (props) => {
const [state, setState] = useState({});
return (
<RootContext.Provider value={[state, setState]}>
{props.children}
</RootContext.Provider>
);
};
and here the part where it goes wrong, in my Content.js
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
import SwipeableRoutes from 'react-swipeable-routes';
import useShow from '../../hooks/useShow';
import NavButton from '../NavButton';
// for this demo we just have one single module component
// when we have real data, there will be a VoteModule and CommentModule at least
// there are 2 important object given to the props; module and match
// module comes from us, match comes from swipeable views library
const ModuleComponent = ({ module, match }) => {
// we need this function from the custom hook
const { setActiveModule } = useShow();
// if this view is active (match.type === 'full') then we tell the show hook that
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match]);
return (
<div style={{ height: 300, backgroundColor: module.title }}>{module.title}</div>
);
};
const Content = () => {
const { modules, previousModule, nextModule } = useShow();
// this is a safety measure, to make sure we don't start rendering stuff when there are no modules yet
if (!modules) {
return <div>Loading...</div>;
}
// this determines which component needs to be rendered for each module
// when we have real data we will switch on module.type or something similar
const getComponentForModule = (module) => {
// this is needed to get both the module and match objects inside the component
// the module object is provided by us and the match object comes from swipeable routes
const ModuleComponentWithProps = (props) => (
<ModuleComponent module={module} {...props} />
);
return ModuleComponentWithProps;
};
// this renders all the modules
// because we return early if there are no modules, we can be sure that here the modules array is always existing
const renderModules = () => (
modules.map((module) => (
<Route
path={`/${module.id}`}
key={module.id}
component={getComponentForModule(module)}
defaultParams={module}
/>
))
);
return (
<div className="content">
<div>
<SwipeableRoutes>
{renderModules()}
</SwipeableRoutes>
<NavButton type="previous" to={previousModule} />
<NavButton type="next" to={nextModule} />
</div>
</div>
);
};
export default Content;
For sake of completion, also my NavButton.js :
import React from 'react';
import { NavLink } from 'react-router-dom';
const NavButton = ({ type, to }) => {
const iconClassName = ['fa'];
if (type === 'next') {
iconClassName.push('fa-arrow-right');
} else {
iconClassName.push('fa-arrow-left');
}
return (
<div className="">
<NavLink className="nav-link-button" to={`/${to}`}>
<i className={iconClassName.join(' ')} />
</NavLink>
</div>
);
};
export default NavButton;
In Content.js there is this part:
// if this view is active (match.type === 'full') then we tell the show hook that
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match]);
which is causing the infinite loop. If I comment out the setActiveModule call, then the infinite loop is gone, but of course then I also won't have the desired outcome.
I am sure I am doing something wrong in either the usage of useEffect and/or the custom hook I have created, but I just can't figure out what it is.
Any help is much appreciated
I think it's the problem with the way you are using the component in the Route.
Try using:
<Route
path={`/${module.id}`}
key={module.id}
component={() => getComponentForModule(module)}
defaultParams={module}
/>
EDIT:
I have a feeling that it's because of your HOC.
Can you try
component={ModuleComponent}
defaultParams={module}
And get the module from the match object.
const ModuleComponent = ({ match }) => {
const {type, module} = match;
const { setActiveModule } = useShow();
useEffect(() => {
if (type === 'full') {
setActiveModule(module);
}
},[module, setActiveModule]);
match is an object and evaluated in the useEffect will always cause the code to be executed. Track match.type instead. Also you need to track the module there. If that's an object, you'll need to wrap it in a deep compare hook: https://github.com/kentcdodds/use-deep-compare-effect
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match.type, module]);

Resources