React array is empty after I filled it in Firebase function - reactjs

I'm trying to load data using React and Firebase.
Unfortunately, I can't get it to display them.
In Firebase's res.items.forEach((itemRef) function, I fill an array.
I would like to access this later. However, this is then empty. How so ?
const array = [];
function loadList() {
const storage = getStorage();
const storageRef = sRef(storage, "images/");
// Find all the prefixes and items.
listAll(storageRef)
.then((res) => {
res.prefixes.forEach((folderRef) => {
// All the prefixes under listRef.
// You may call listAll() recursively on them.
});
res.items.forEach((itemRef) => {
array.push(itemRef._location);
// All the items under listRef.
console.log(itemRef._location);
});
})
.catch((error) => {
// Uh-oh, an error occurred!
});
}
function App() {
loadList();
return (
<div className="App">
<Stack direction="row" alignItems="center" spacing={2}>
// ARRAY IS EMPTY
{array?.map((value, key) => {
return <h1>{value}</h1>;
})}
...

From your explaination, here is what I gathered you are trying to do:
Fetch the documents from cloud firestore.
Store them in an array.
Create a list of Components in the browser view rendered by react using the array that was fetched.
For such, fetching and rendering tasks, your best bet would be using Callbacks, State and Effect.
So:
State would hold the values for react to render.
Effect will fetch the data when the component loads.
Callback will do the actual fetching because asynchronous fetching on useEffect directly is discouraged.
const storage = getStorage();
const listRef = ref(storage, 'files/uid');
function ImageApp()
{
// This is the array state of items that react will eventually render
// It is set to empty initially because we will have to fetch the data
const [items, setItems] = React.useState([]);
React.useEffect(() =>
{
fetchItemsFromFirebase();
}, []); // <- Empty array means this will only run when ImageApp is intially rendered, similar to onComponentMount for class
const fetchItemsFromFirebase = React.useCallback(async () =>
{
await listAll(listRef)
.then((res) =>
{
// If (res.items) is already an array, then use the method below, it would be faster than iterating over every single location
// cosnt values = res.items.map((item) => item._location);
// If res.items doesnt already return an array, then you unfortunately have to add each item individually
const values = [];
for (const itemRef of res.items)
{
values.push(itemRef._location);
}
console.log(values);
setItems(values);
})
.catch((error) => { console.error(error) });
}, []); // <- add "useState" values inside the array if you want the fetch to happen every smth it changes
return (
<div className="App">
{
items && // Makes sure the fragment below will only be rendered if `items` is not undefined
// An empty array is not undefined so this will always return false, but its good to use for the future :)
<>
{
(items.map((item, itemIndex) =>
<h1 key={ itemIndex }>{ item }</h1>
))
}
</>
}
</div>
)
}

Related

Trying to get data from api and map to another component in React

I'm trying to map an array of movies which I get from an API.
The data is fetched successfully but when I try to map the values and display, it becomes undefined and does not show anything.
I'm new to React so any help and advice would be helpful.
const [items, setItems] = useState([]);
const getMovieData = () => {
axios
.get(api_url)
.then((response) => {
const allMovies = response.data;
console.log(allMovies);
setItems(allMovies);
})
.catch((error) => console.error(`Error: ${error}`));
};
useEffect(() => {
getMovieData();
}, []);
return (
<div>
{items.map((item) => {
<p>{item.title}</p>;
})}
</div>
);
The data is stored like this:
0: {
adult: false,
backdrop_path: '/9eAn20y26wtB3aet7w9lHjuSgZ3.jpg',
id: 507086,
title: 'Jurassic World Dominion',
original_language: 'en',
...
}
You're not returning anything from your map
{
items.map((item) => {
// Add a return
return <p>{item.title}</p>
})
}
First, your items value is an empty array[] as you have initialized with setState([]) and your useEffect() runs only after your component is rendered which means even before you could do your data fetching, your HTML is being displayed inside which you are trying to get {item.title} where your items is an empty array currently and hence undefined. You will face this issue often as you learn along. So if you want to populate paragraph tag with item.title you should fast check if your items is an empty array or not and only after that you can do the mapping as follow and also you need to return the element from the map callback. If it takes some time to fetch the data, you can choose to display a loading indicator as well.
const [items, setItems] = useState([]);
const getMovieData = () => {
axios.get(api_url)
.then((response) => {
const allMovies = response.data;
console.log(allMovies);
setItems(allMovies);
}).catch(error => console.error(`Error: ${error}`));
};
useEffect(() => {
getMovieData();
}, []);
return ( < div > {
items.length !== 0 ? items.map((item) => {
return <p > {
item.title
} < /p>
}) : < LoadingComponent / >
}
<
/div>
);
Good catch by Ryan Zeelie, I did not see it.
Another thing, since you're using promises and waiting for data to retrieve, a good practice is to check if data is present before mapping.
Something like :
return (
<div>
{ (items.length === 0) ? <p>Loading...</p> : items.map( (item)=>{
<p>{item.title}</p>
})}
</div>
);
Basically, if the array is empty (data is not retrieved or data is empty), display a loading instead of mapping the empty array.

Mapping into firestore from React

I am new to react and firebase/firestore.
I am trying to map into what I believe to be a nested firestore value. I am able to pull each value individually
function Pull() {
const [blogs,setBlogs]=useState([])
const fetchBlogs=async()=>{
const response=firestore.collection('customer');
const data= await response.get();
data.docs.forEach(item=>{
setBlogs(data.docs.map(d => d.data()))
console.log(data)
})
}
useEffect(() => {
fetchBlogs();
}, [])
return (
<div className="App">
{
blogs.map((items)=>(
<div>
<p>{items[1].name}</p>
</div>
))
}
</div>
);
}
I have been trying to map twice to get into the string inside the collection, yet I have had no luck.
My FireStore collection
https://drive.google.com/file/d/1Erfi2CVrBSbWocQXGR5PB_ozgg9KEu12/view?usp=sharing
Thank you for your time!
If you are iterating a data.docs array and enqueueing multiple state updates then you will want to use a functional state update to correctly enqueue, and update from the previous state.
const fetchBlogs = async ( )=> {
const response = firestore.collection('customer');
const data = await response.get();
data.docs.forEach(item => {
setBlogs(blogs => blogs.concat(item.data()))
});
}
or you can map the data.docs to an array of items and update state once.
const fetchBlogs = async ( )=> {
const response = firestore.collection('customer');
const data = await response.get();
setBlogs(blogs => blogs.concat(data.docs.map(item => item.data())));
}
try changing the foreach to a snapshot like this:
data.docs.onSnapshot(snapshot=>{
setBlogs(snapshot.docs.map(d => d.data()))
console.log(data)
})
ive used it like this in the past multiple times
and it has worked. If it doesnt work, the instagram clone tutorial on youtube by Clever Programmer goes over this well.

Am I using the useEffect hook correctly to try and fetch data?

I am having some issues with my React code when trying to use the useEffect hook to fetch data from an API.
This is my boiled down code:
const App = () => {
const [ data, setData ] = useState([]);
const [ loading, setLoading ] = useState(false);
useEffect(() => {
const loadData = async () => {
try {
setLoading(true);
const response = await fetch(`htts://localhost/api/`);
const json = await response.json();
console.log(json);
setData(json);
setLoading(false);
} catch (error) {
console.log("error: " + error);
}
}
loadData();
}, []);
}
I am running into issues that when I render the html it's giving me errors that I am unable to map to some of the data that I am setting from my API.
I was thinking that setting the data variable would give me the ability to spit out data in the template and the various object key names like:
{
data.keyname.map((item, index) => {
return (
<option key={ index }
value={ item.displayName }
>
{ item.displayName }
</option>
);
})
}
Am I overlooking something here? It appears that my data isn't loading so the render code can't loop through any of the data it needs to in order to render the app.
If your try to map anything in return of your component, it has to be initialized as an array. It doesn't matter if its empty, but always has to be an array.
So i suggest you initialize your "data" like this:
const [ data, setData ] = useState({anyKeyName:[]});
What is data.keyname? data starts out as an array:
const [ data, setData ] = useState([]);
And an array has no keyname property. So this will fail with an error saying you can't invoke .map on undefined:
data.keyname.map((item, index) => {
If what you're getting back from the server is expected to be an object which has an array property called keyname, initialize your default object to match that:
const [ data, setData ] = useState({ keyname: [] });
Basically it sounds like your useEffect logic is working (assuming the response is what you expect it to be), but you simply have a mis-match between your initial state and what you expect that state to be when rendering.
Adding Optional chaining(?) is the quick and solid solution for this.
{
data?.keyname?.map((item, index) => {
return (
<option key={ index }
value={ item.displayName }
>
{ item.displayName }
</option>
);
})
}
Learn more about Optional_chaining.
Initializing data as an empty array without handling the other fetching states is opening your app up to UI bugs and hard errors - which all can be avoided if you handle all of the data fetching states.
There are at least 4 states that are traditionally handled when fetching data -
Data not fetched (data null)
Fetching (loading)
Error fetching (error)
Data Fetched (data)
See below for an example on how to handle all 4 states without breaking the app's UI if something goes wrong or no data is returned.
const App = () => {
const [ data, setData ] = useState(null);
const [ error, setError ] = useState(false);
const [ loading, setLoading ] = useState(false);
useEffect(() => {
const loadData = async () => {
setLoading(true);
try {
const response = await fetch(`htts://localhost/api/`);
// response is not asynchronous - don't await.
setData(response.json());
} catch (error) {
console.error("error: ", error); // make sure you remove this on build - you can also use common instead of concatenation
setError(true)
} finally {
setLoading(false); // move loading state so it always exits the loading state on error or success - currently if yours has an error your app will be stuck in loading state.
}
}
loadData();
}, []);
if( loading ) return <>Loading component</>;
if( error ) return <>Error component </>;
// check if data was returned and verify it's not just an empty array
if (!data || !data.keyname.length) return <>No data found component </>;
return (
<>
{data.keyname.map(({ displayName, id }) => (
<option
key={ id } // should use something unique to this set of elements (not index) like item.id - if the id is auto incremented then prefix it with something unique for this component like `keynames-${id}`
value={ displayName }
>
{ displayName }
</option>
)}
</>
)
}

ReactJS: counting viewed children in the parent class without knowing their status (receiving it from children)

I have a search that fetches items and displays them. After a click on an item, it marked as viewed. And it is recorded to a database so some items are viewed without any clicks.
The goal is: count viewed items in the parent class (only items that are currently displayed)
How I do it: I trigger the function from the parent element which increments "viewed" variable.
The problem is: items fetched many times and sometimes you receive the same items as before, sometimes same + more additional items, sometimes completely new items.
I can't figure out when to reset count as on new fetch old items don't trigger "handleIsViewed". It collects viewed items from multiple fetches or shows 0 even one of the items viewed already, but didn't re-rendered on new fetch (as it was displayed before new fetch happened)
This is simplified version of the code I'm using:
export default function Results(props) {
const [items, setItems] = useState([]); // items array
const [viewed, setViewedCount] = useState(0); // this doesn't reset state when new fetch going
// this will trigger fetch of new items
const handleOnClick = () => {
// setViewedCount(0); // when resetting viewed count here, on the new fetch with the same items it will stay 0
fetch("/getitems", {
// skipped
})
.then(res => res.json())
.then(
(result) => {
setItems(result.items);
}
);
}
// counting viewed matches (triggered in children)
const handleIsViewed = () => {
setViewedCount(prevState => {
return 1 + prevState;
});
}
// passing function "handleIsViewed" to items so they can trigger it
let rows = [];
for (let item of items) {
rows.push(<Item key={item._id} {...item} handleIsViewed={handleIsViewed} />);
}
return (<div>
<div>{rows}</div>
<button onClick={handleOnClick}>Fetch items</button>
</div>);
}
export default function Item(props) {
const [viewed, setViewed] = useState(false);
useEffect(() => {
// checking if its viewed or no in db
if (true == marked_viewed_in_database) {
setViewed(false);
handleIsViewed(props._id);
}
}, []);
// click will mark item as viewed
const handleClick = (evt) => {
// if already viwed doing nothing
if (viewed) return;
setViewed(true);
props.handleIsViewed(props._id);
}
return (
<div onClick={handleClick}>item</div>
);
};
The solution I found:
I trigger handleIsViewed in the child outside of useEffect. In this case it will be triggered each time new items received, even some not rendered (because rendered before that).
In this case, the iterator provides duplicates on viewed data, so I store ids of each match in the set (and without using it as a state variable) and then use viewed.size to get viewed items count.
export default function Results(props) {
const [items, setItems] = useState([]); // items array
let viewed = new Set(); // will be resettings each time new fetch triggered
// this will trigger fetch of new items
const handleOnClick = () => {
// setViewedCount(0); // when resetting viewed count here, on the new fetch with the same items it will stay 0
fetch("/getitems", {
// skipped
})
.then(res => res.json())
.then(
(result) => {
setItems(result.items);
}
);
}
// counting viewed matches (triggered in children)
const handleIsViewed = (id) => {
viewed.add(id);
}
// passing function "handleIsViewed" to items so they can trigger it
let rows = [];
for (let item of items) {
rows.push(<Item key={item._id} {...item} handleIsViewed={handleIsViewed} />);
}
return (<div>
<div>{rows}</div>
<button onClick={handleOnClick}>Fetch items</button>
</div>);
}
export default function Item(props) {
const [viewed, setViewed] = useState(false);
useEffect(() => {
// checking if its viewed or no in db
if (true == marked_viewed_in_database) {
setViewed(false);
handleIsViewed(props._id);
}
}, []);
// sending data on each fetch update from the child
if (viewed) {
props.handleIsViewed(props._id);
}
// click will mark item as viewed
const handleClick = (evt) => {
// if already viwed doing nothing
if (viewed) return;
setViewed(true);
props.handleIsViewed(props._id);
}
return (
<div onClick={handleClick}>item</div>
);
};

React Hooks LocalStorage setState override elements

I'm struggling with React hooks using useEffect in case of storing tasks in localstorage, so refreshing page will still handle the elements from the list. The problem is while I'm trying to get the elements from LocalStorage and set them for todos state. They exist inside localStorage, but element that is inside todos state is only one from localstorage.
Here is a Component that handling this:
const [todos, setTodos] = useState([]);
const [todo, setTodo] = useState("");
const addTask = (event) => {
event.preventDefault();
let newTask = {
task: todo,
id: Date.now(),
completed: false,
};
setTodos([...todos, newTask]);
setTodo("");
};
const getLocalStorage = () => {
for (let key in localStorage) {
if (!localStorage.hasOwnProperty(key)) {
continue;
}
let value = localStorage.getItem(key);
try {
value = JSON.parse(value);
setTodos({ [key]: value });
} catch (event) {
setTodos({ [key]: value });
}
}
};
const saveLocalStorage = () => {
for (let key in todos) {
localStorage.setItem(key, JSON.stringify(todos[key]));
}
};
useEffect(() => {
getLocalStorage();
}, []);
useEffect(() => {
saveLocalStorage();
}, [saveLocalStorage]);
const testClick = () => {
console.log(todos);
};
return (
<div className="App">
<h1>What would you like to do?</h1>
<TodoForm
todos={todos}
value={todo}
inputChangeHandler={inputChangeHandler}
addTask={addTask}
removeCompleted={removeCompleted}
/>
{/* <TodoList todos={todos} toogleCompleted={toogleCompleted} /> */}
<button onClick={testClick}>DISPLAY TODOS</button>
</div>
);
Second problematic error while refreshing page:
Apart from that I can not display elements after getting them from local storage. Probably I'm missing some stupid little thing, but I stuck in this moment and I was wondering if small explanation would be possible where I'm making mistake.
I think you are overwriting the state value all together. You want to append to what exists in the todo state. You could use the spread operator to set the todo to everything that's already in there plus the new value.
Something like this:
setTodos({...todos, [key]: value });
setTodos([...todos, [key]: value ]);
Note that spread operator is a shallow clone. If you have a deeper object (more than two layers i think) then use something like cloneDeep from the lodash library.
To see whats happening change this block to add the console.logs
try {
value = JSON.parse(value);
setTodos({ [key]: value });
console.log(todos);
} catch (event) {
console.log('There was an error:', error);
// Should handle the error here rather than try to do what failed again
//setTodos({ [key]: value });
}
Edit: regarding the error you added.
You are trying to use map on an object. map is an array function.
Instead you would convert your object to an array and then back:
const myObjAsArr = Object.entries(todos).map(([k,v]) => {
// Do stuff with key and value
}
const backToObject = Object.fromEntries(myObjAsArr);
Change this to set an array (not object):
setTodos([...todos, [key]: value ]);
Put console logs everywhere so you can see whats happening.

Resources