React - Updating previous array with new values using useState hook - javascript

I created a home page that contains three components HomeHeader, CompanyList and ScrollToTopBtn.
Focusing on CompanyList it is a container component that shows list of CompanyCards that were fetched using an API in Home page. List of companies are initialized in Home page like this
const [companies, setCompanies] = useState([]);
The problem is that initially after making the API call, I use spread operator to update the companies list like this
const fetchedCompanies = await fetchFeedData(page);
console.log(fetchedCompanies);
setCompanies((prevCompanies=>{return [...prevCompanies,fetchedCompanies]}));
console.log(companies);
But an error occurs Uncaught TypeError: prevCompanies is not iterable since I believe that companies list is initially empty.
I tried to use another approach by using concat method but companies list stayed empty and showed no companies found message.
Below is the source code for Home page
const Home = () => {
const [page, setPage] = useState(1); //In order to avoid showing "no companies found" initially
const [isLoading, setIsLoading] = useState(true);
const [companies, setCompanies] = useState([]);
const [isError, setIsError] = useState(false);
const [hasMore, setHasMore] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
setIsError(false);
try {
const fetchedCompanies = await fetchFeedData(page);
setCompanies((prevCompanies=>{prevCompanies.concat(fetchedCompanies)})); //problem here
setHasMore(fetchedCompanies.length > 0)
} catch (e) {
setIsError(true)
setHasMore(false);
}
setIsLoading(false)
}
fetchData();
}, [page])
return (
<div>
<ScrollToTopBtn />
<Helmet>
<title>{stackinfo.title} - {home.title}</title>
</Helmet>
<HomeHeader />
{
isLoading ? (<LoadingSpinner />) :
isError ? (<ErrorBoundary message={somethingWrongError.message}></ErrorBoundary>) :
<CompanyList companies={companies} ></CompanyList>
}
<Container className={"mt-5"}>
<Row>
<Col sm={12}>
{(!isLoading && !isError && hasMore) &&
<CustomBtn
ButtonText={showMoreBtnText.message}
onClick={() => { console.log("Need to increment number of pages") }}
></CustomBtn>}
</Col>
</Row>
</Container>
</div>
)
}
I tried to check the fetchedCompanies and companies after making the API call
const fetchedCompanies = await fetchFeedData(page);
//Returning new array containing previous companies and recent fetchedCompanies
console.log(fetchedCompanies);
setCompanies((prevCompanies=>{prevCompanies.concat(fetchedCompanies)}));
console.log(companies);
fetchedCompanies logged an array that has 9 elements, while companies as mentioned above logged empty array [].
Sorry if I missed something, I am still new to React.

You can do this below to update
setCompanies([...companies, ...fetchedCompanies])
If fetched Company are totally new Array containing all with previous record then just do it below;
setCompanies([...fetchedCompanies]);
//OR
setCompanies(fetchedCompanies);
If you have empty strings then do this below
setCompanies([...companies, ...fetchedCompanies.filter(com => !com)]);

This is trying to mutate state, not return a new state (or maybe you just forgot the return keyword?):
setCompanies((prevCompanies=>{prevCompanies.concat(fetchedCompanies)}));
Your previous attempt was closer:
setCompanies(prevCompanies=>{return [...prevCompanies, fetchedCompanies]});
But if fetchedCompanies is also an array as the name implies then you forgot its spread operator:
setCompanies(prevCompanies=>{return [...prevCompanies, ...fetchedCompanies]});
Without that the resulting array would be weird at best.
You can also simplify a little:
setCompanies(prevCompanies=>[...prevCompanies, ...fetchedCompanies]);
And if you don't expect these calls to overlap at all, you could potentially simplify a lot:
setCompanies([...companies, ...fetchedCompanies]);
If after that there is still an empty string then it seems that the data has an empty string. In that case you'd have to filter that out manually, and where you do that is really up to you if the act of filtering might mess with the rest of the logic you have there (the hasMore value, for example). But you can append .filter() to the resulting array any time you like. When fetching, when updating state, or even just when rendering.

You would be better to look where the empty string is coming from and try to resolve that issue but if you simple want to remove the empty string, use the following.
prevCompanies.concat(fetchedCompanies).filter(Boolean)

Related

REACT Duplicated Key Warning

I am currently get this duplicated key warning "Warning: Encountered two children with the same key". However, I am unsure where this duplication of key comes from. I am using the fileData id as my key which should be unique as it is firebase generated id. Therefore, I am not so sure what is happening behind here.
Here are my codes below and the warning I get.
MultimediaDetails.js
import React, { useEffect, useState } from "react";
import * as AiIcons from "react-icons/ai";
import * as FaIcons from "react-icons/fa";
import { database } from "../../../firebase";
import ViewImageFileModal from "../../modals/multimediaModals/view/ViewImageFileModal";
/**
* It's a component that displays audio, video, and image files
* #param props - The props object that is passed to the component.
* #returns The MultimediaDetails component is being returned.
*/
const MultimediaDetails = (props) => {
/* Destructuring the props object. */
const { pId } = props;
/* Setting the state of the component. */
const [imageData, setImageData] = useState([]);
const [imageMessage, setImageMessage] = useState(true);
const userType = JSON.parse(localStorage.getItem("admin") ?? false);
// Modal Variables
const [showViewImageModal, setShowViewImageModal] = useState(false);
const [fileData, setFileData] = useState(Object);
/**
* When the user clicks on the audio, video, or image file, the file data is set and the modal is
* toggled.
* #param obj
*/
const viewImageFile = (obj) => {
setFileData(obj);
toggleViewImageModal();
};
/* The function to toggle modal states */
const toggleAddImageModal = () => setShowAddImageModal((p) => !p);
const toggleViewImageModal = () => setShowViewImageModal((p) => !p);
useEffect(() => {
/* Query data from database and listening for changes. */
const imageQuery = database.portfolioRef.doc(pId).collection("images");
const unsubscribeImage = imageQuery.onSnapshot((snapshot) => {
if (snapshot.docs.length !== 0) {
setImageMessage(false);
setImageData(
snapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }))
);
} else {
setImageMessage(true);
}
});
return () => {
unsubscribeImage();
};
}, [pId]);
return (
<div className="multimedia-section">
<div id="image-section">
<div id="image-header">
<h6>
<u>Images</u>
</h6>
{userType ? (
<button className="addbtn" onClick={() => toggleAddImageModal()}>
<AiIcons.AiOutlinePlus /> Add Image File
</button>
) : (
<></>
)}
</div>
<div id="image-content" className="multimedia-flex">
{imageMessage ? (
<p>There is not existing images for this portfolio.</p>
) : (
<div>
{imageData.map((doc) => (
<button
key={doc.id}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
</div>
)}
</div>
</div>
<ViewImageFileModal
show={showViewImageModal}
toggleModal={toggleViewImageModal}
pId={pId}
data={fileData}
key={fileData.id}
/>
</div>
);
};
export default MultimediaDetails;
The initialised values for the Modal.
/* Setting the initial state of the component. */
const valueState = {
name: '',
description: ''
}
const { currentUser } = useAuth();
const [formStateDisabled, setFormStateDisabled] = useState(true);
const [deleteState, setDeleteState] = useState(false);
const [message, setMessage] = useState('');
const [imageUrl, setImageUrl] = useState("");
const [loadForm, setLoadForm] = useState(false)
const [view, setView] = useState(false);
/* Destructuring the props object. */
const { show, toggleModal } = props;
const { handleChange, handleSubmit, values, errors, loading } =
useForm(validateUpdate, valueState, handleUpdate);
useEffect(() => {
if (Object.keys(props.data).length !== 0) {
values.name = props.data.imageName;
values.description = props.data.imageDesc;
setLoadForm(true);
}
}, [])
The warning I get (Shown Below), each time I click on the modal button to open the button, I noticed the warning actually repeats twice, and when I close it, it repeats another 2 times making it 4. I am not sure what is the cause of this, please help! Thank you!
Updates of trials
I only have 4 rows of data, all of which has its own unique id. Therefore I am unsure of where the duplicated key came from. However, if I remove the modal key "fileData.id" this warning would disappear. However, my component state will not reset and there will be a lot of props data issue that would surface. Where data for the previously clicked button will appear on the another button. Or the data might not appear at all.
FOR ADDITIONAL INFORMATION:
This is the output for the map buttons
I don't see any duplicates, and I am not sure where the issue is. Is there something I am doing wrong to cause this error. I checked my DB there isn't any data error as well.
Recommended solution
The problem is that your doc.id is repeating.
You are setting the imageData at the imageQuery.onSnapshot callback function, when you run the following code:
setImageData(snapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
What you need to make sure is that doc.id is unique in this context (because you're using this value at the key attribute in your buttons).
That's the correct way to fix it.
Alternative solution
Another way to handle it (as a last resort), is using the following code, where you use the index position of the element at the key attribute:
{imageData.map((doc, index) => (
<button
key={index}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
But this is not recommended according to the React documentation:
We don’t recommend using indexes for keys if the order of items may
change. This can negatively impact performance and may cause issues
with component state. Check out Robin Pokorny’s article for an
in-depth explanation on the negative impacts of using an index as a key.
If you choose not to assign an explicit key to list items then
React will default to using indexes as keys.
Here is an in-depth explanation about why keys are necessary if you’re
interested in learning more.
Instead of doc.id use map item index like bellow. See if it works.
{imageData.map((index, doc) => (
<button
key={index}
className="fileBtn"
onClick={() => viewImageFile(doc)}>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
This is the culprit:
{imageData.map((doc) => (
<button
key={doc.id}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
The main issue here is that doc.id is duplicate, probably you have duplicate data in your imageData or ou have a faulty data in your database or something that generate a non-unique id.
To easily fix the issue, what you can do is use index of map.
{imageData.map((doc, index) => (
<button
key={index}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
index are always unique. but I suggest you should fix and see why you have duplicate data instead of just bypassing it with an index.
UPDATE
This is quite a hacky solution, but since I can't really pin point what's causing the issue without investigating first hand, let's make it so you don't have to pass a key on the modal.
So instead of storing the object data on the state, store the id instead:
Rename fileData to fileDataId.
const [fileDataId, setFileDataId] = useState(0);
then store the id when clicking the button.
{imageData.map((doc) => (
<button
key={doc.id}
className="fileBtn"
onClick={() => viewImageFile(doc.id)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
on the Modal, you have to pass the imageData and the selected id, then remove the key:
<ViewImageFileModal
show={showViewImageModal}
toggleModal={toggleViewImageModal}
pId={pId}
list={imageData}
selectedId={fileDataId}
/>
then inside ViewImageFileModal you can declare data as:
const data= props.list.find(image => image.id === props.selectedId);
I just wrap my model with a condition if imageId exist and it works already!
{imageId !== '' &&
<ViewImageFileModal
show={showViewImageModal}
toggleModal={toggleViewImageModal}
imageData={imageData}
key={imageId}
/>
}

Have a custom hook run an API call only once in useEffect

I have a custom hook in my React application which uses a GET request to fetch some data from the MongoDB Database. In one of my components, I'm reusing the hook twice, each using different functions that make asynchronous API calls.
While I was looking at the database logs, I realized each of my GET requests were being called twice instead of once. As in, each of my hooks were called twice, making the number of API calls to be four instead of two. I'm not sure why that happens; I'm guessing the async calls result in re-renders that aren't concurrent, or there's somewhere in my component which is causing the re-render; not sure.
Here's what shows up on my MongoDB logs when I load a component:
I've tried passing an empty array to limit the amount of time it runs, however that prevents fetching on reload. Is there a way to adjust the custom hook to have the API call run only once for each hook?
Here is the custom hook which I'm using:
const useFetchMongoField = (user, id, fetchFunction) => {
const [hasFetched, setHasFetched] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
if (!user) return;
try {
let result = await fetchFunction(user.email, id);
setData(result);
setHasFetched(true);
} catch (error) {
setError(error.message);
}
};
if (data === null) {
fetchData();
}
}, [user, id, fetchFunction, data]);
return { data, hasFetched, error };
};
This is one of the components where I'm re-using the custom hook twice. In this example, getPercentageRead and getNotes are the functions that are being called twice on MongoDB (two getPercentageRead calls and two getNotes calls), even though I tend to use each of them once.
const Book = ({ location }) => {
const { user } = useAuth0();
const isbn = queryString.parse(location.search).id;
const { data: book, hasFetched: fetchedBook } = useFetchGoogleBook(isbn);
const { data: read, hasFetched: fetchedPercentageRead } = useFetchMongoField(
user,
isbn,
getPercentageRead
);
const { data: notes, hasFetched: fetchedNotes } = useFetchMongoField(
user,
isbn,
getNotes
);
if (isbn === null) {
return <RedirectHome />;
}
return (
<Layout>
<Header header="Book" subheader="In your library" />
{fetchedBook && fetchedPercentageRead && (
<BookContainer
cover={book.cover}
title={book.title}
author={book.author}
date={book.date}
desc={book.desc}
category={book.category}
length={book.length}
avgRating={book.avgRating}
ratings={book.ratings}
language={book.language}
isbn={book.isbn}
username={user.email}
deleteButton={true}
redirectAfterDelete={"/"}
>
<ReadingProgress
percentage={read}
isbn={book.isbn}
user={user.email}
/>
</BookContainer>
)}
{!fetchedBook && (
<Wrapper minHeight="50vh">
<Loading
minHeight="30vh"
src={LoadingIcon}
alt="Loading icon"
className="rotating"
/>
</Wrapper>
)}
<Header header="Notes" subheader="All your notes on this book">
<AddNoteButton
to="/add-note"
state={{
isbn: isbn,
user: user,
}}
>
<AddIcon color="#6b6b6b" />
Add Note
</AddNoteButton>
</Header>
{fetchedNotes && (
<NoteContainer>
{notes.map((note) => {
return (
<NoteBlock
title={note.noteTitle}
date={note.date}
key={note._noteID}
noteID={note._noteID}
bookID={isbn}
/>
);
})}
{notes.length === 0 && (
<NoNotesMessage>
You don't have any notes for this book yet.
</NoNotesMessage>
)}
</NoteContainer>
)}
</Layout>
);
};
The way you have written your fetch functionality in your custom hook useFetchMongoField you have no flag to indicate that a request was already issued and you are currently just waiting for the response. So whenever any property in your useEffect dependency array changes, your request will be issued a second time, or a third time, or more. As long as no response came back.
You can just set a bool flag when you start to send a request, and check that flag in your useEffect before sending a request.
It may be the case that user and isbn are not set initially, and when they are set they each will trigger a re-render, and will trigger a re-evalution of your hook and will trigger your useEffect.
I was able to fix this issue.
The problem was I was assuming the user object was remaining the same across renders, but some of its properties did in fact change. I was only interested in checking the email property of this object which doesn't change, so I only passed user?.email to the dependency array which solved the problem.

why do i get Error: Too many re-renders. React limits the number of renders to prevent an infinite loop

In my React.js code Im fetching from an API and I m getting this error
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
PLEASE WHAT AM I DOING WRONG?
Here's my fetch component in App.js
const App = () => {
const [records, setRecords] = useState([])
const [loading, setLoading] = useState({ loading: false })
useEffect(() => {
(async function() {
try {
setLoading(true)
const res = await fetch('http://api.enye.tech/v1/challenge/records?limit=20&skip=0')
const data = await res.json();
setRecords({ records: data})
setLoading(false)
console.log(data);
} catch (err) {
console.error(err);
}
})()
}, [])
if(loading) {
return <Preloader />
}
return (
<Fragment>
<SearchBar />
<div className='container'>
<Records loading={setLoading()} records={setRecords()} />
</div>
</Fragment>
);
}
And this is where I'm passing in the fetched data as props
const Records = ({ records, loading }) => {
return (
<div className={styles.p__container}>
<div className="row">
<div className="col-sm-8">
<div className="py-3">
{records.length} Records
</div>
</div>
</div>
<div className={styles.p__grid}>
{records.map(record => (
<RecordItem key={record.id} record={record} />
))
}
</div>
<div className={styles.p__footer}>
</div>
</div>
)
My integrated terminal shows no error but I get this error in my browser console
Im also trying to see if I can fetch just 20 profiles from the API instead of 100
http://api.enye.tech/v1/challenge/records
It's likely here, when you call setLoading() and setRecords on every render:
<Records loading={setLoading()} records={setRecords()} />
You probably just want to pass a the loading and records variables:
<Records loading={loading} records={records} />
The error is in the line
<Records loading={setLoading()} records={setRecords()} />
By writing setLoading() and setRecords() you are essentially changing the states loading, and records. This ensures a re-render, since states are being changed
I believe, you are trying to pass the current loading status and records array as props to component Records. To do so, you should pass the values like
<Records loading={loading} records={records} />
That should clear out the too many re-renders error
Suggestion on useState usage
You are initializing the states loading and setRecords in a different way than the way you are using it later.
For eg., you initialize loading as {loading: false} but later call setLoading(false). You should initialize the loading state as a boolean state using
const [loading, setLoading] = useState(false);
// example usage
setLoading(true);
Similarly, for records, you are initializing with an empty array useState([]), but later setting records state as an object using setRecords({ records: data})
You should use either one of the approaches
/** Approach 1 (Array typed) */
const [records, setRecords] = useState([]);
// example usage
setRecords(data);
/** Approach 2 (Object typed) */
const [records, setRecords] = useState({records: []});
// example usage
setRecords({records: data});
React has a property that whenever the value of states in a React component get updated, the component is re-rendered. Here you are passing the setLoading() and setRecords() as props to the Records component. You should pass records and loading instead. Every time setLoading() and setRecords() get updated, your component is re rendered and as a result an infinite loop of re rendering takes place.

React Native load data from API using hooks

Im new in ReactNative and I'm trying to take some data from here https://www.dystans.org/route.json?stops=Hamburg|Berlin
When I try console.log results it return full API response. I dont know why in first results.distance works and return distance, but when I'm trying to do it inside FlatList nothing is returned. Sometimes it works when i want to return only item.distance but can't somethnig like <Text>{item.stops[0].nearByCities[0].city}</Text> nowhere in my code also in console. Im getting error:
undefined is not an object (evaluating 'results.stops[0]')
imports...
const NewOrContinueScreen = ({ navigation }) => {
const [searchApi, results, errorMessage] = useDystans();
console.log(results.distance);
return (
<SafeAreaView forceInset={{ top: "always" }}>
<Text h3 style={styles.text}>
Distance: {results.distance}
</Text>
<Spacer />
<FlatList
extraData={true}
data={results}
renderItem={({ item }) => (
<Text>{item.distance}</Text>
// <Text>{item.stops[0].nearByCities[0].city}</Text>
)}
keyExtractor={item => item.distance}
/>
<Spacer />
</SafeAreaView>
);
};
const styles = StyleSheet.create({});
export default NewOrContinueScreen;
And here is my hook code:
import { useEffect, useState } from "react";
import dystans from "../api/dystans";
export default () => {
const [results, setResults] = useState([]);
const [errorMessage, setErrorMessage] = useState("");
const searchApi = async () => {
try {
const response = await dystans.get("route.json?stops=Hamburg|Berlin", {});
setResults(response.data);
} catch (err) {
setErrorMessage("Something went wrong with useDystans");
}
};
useEffect(() => {
searchApi();
}, []);
return [searchApi, results, errorMessage];
};
As the name implies, FlatList is designed to render a list. Your API endpoint returns a JSON Object, not an Array, so there's nothing for the FlatList to iterate. If you want to show all the stops in the list, try passing in the stops list directly.
<FlatList
data={results.stops}
renderItem={({ item }) => (<Text>{item.nearByCities[0].city}</Text>)}
/>
Some side notes: (1) The extraData parameter is used to indicate if the list should re-render when a variable other than data changes. I don't think you need it here at all, but even if you did, passing in true wouldn't have any effect, you need to pass it the name(s) of the variable(s). (2) The keyExtractor parameter is used to key the rendered items from a field inside of them. The stop objects from the API don't have a member called distance so what you had there won't work. From my quick look at the API response, I didn't see any unique IDs for the stops, so you're probably better off letting React key them from the index automatically.

Can't access array data from API call when using map() in React

I'm attempting to map over data I received from an API call. Getting shallow endpoints works fine, but anything nested gives me an error.
The goal is to get all of the opening themes and display them in a 'ul'.
The exact error "TypeError: anime.opening_themes is undefined"
Repo to the project
Heres the endpoints.
Heres my component.
const AnimeDetails = (props) => {
const API = 'https://api.jikan.moe/v3/anime'
const initialState = {
anime: []
}
const [anime, setAnime] = useState(initialState)
useEffect(() => {
const getAnime = async () => {
const response = await fetch(`${API}/${props.match.params.animeId}`)
const data = await response.json()
console.log(data);
setAnime(data) // set initial state to hold data from our API call
}
getAnime()
}, []) // [] prevents useEffect from running in an infinite loop
return (
<AnimeDetailsWrapper>
<Title>{anime.title}</Title>
<Details>
{anime.opening_themes
.map((song, index) => (
<li key={index}>{song}</li>
))}
</Details>
</AnimeDetailsWrapper>
)
}
Your initial state is an empty array, not an empty object:
const initialState = {
anime: []
}
When your component mounts, there is no data yet, so you're attempting to render [].opening_themes.map, and obviously there is no opening_themes property on an empty array.
Set your initial state to an empty object instead:
const initialState = {}
And you will probably want to test that you have data before attempting to render it:
return anime.mal_id && (
<AnimeDetailsWrapper>
<Title>{anime.title}</Title>
<Details>
{anime.opening_themes
.map((song, index) => (
<li key={index}>{song}</li>
))}
</Details>
</AnimeDetailsWrapper>
)
This will prevent your component from rendering anything until your anime state contains a mal_id property.
The first time you render your component, the state anime is equal to {anime: []}, which has no property called opening_themes.
you should try like this
first, remove initialState code and use direct code like following
if the response is in the form of an array
const [anime, setAnime] = useState([])
if the response is in the form of an object
const [anime, setAnime] = useState({})
otherwise, null will work with any response
const [anime, setAnime] = useState(null)
return code like this
return (
<> {anime && <AnimeDetailsWrapper>
<Title>{anime.title}</Title>
<Details>
{anime.opening_themes
.map((song, index) => (
<li key={index}>{song}</li>
))}
</Details>
</AnimeDetailsWrapper>}</>
)

Categories